From 7c4949b0d091181206c0a5e3fdf036be031c0fa2 Mon Sep 17 00:00:00 2001 From: "Kasiewicz, Marek" Date: Mon, 29 Jun 2026 07:55:24 +0000 Subject: [PATCH 01/19] Refactor: Standardize pacing structures to long double precision Promote pacing structures (audio, video, ancillary, fastmetadata) and their pacing cursors such as trs, frame_time, tsc_time_cursor, and ptp_time_cursor to long double. Ensure pacing calculations are carried out inside high-precision types and utilize nextafterl() instead of double-precision variants to prevent precision loss. Modify logging and print patterns to use %Lf for the updated members. Signed-off-by: Kasiewicz, Marek --- lib/src/st2110/st_header.h | 40 ++++++++++----------- lib/src/st2110/st_tx_ancillary_session.c | 8 ++--- lib/src/st2110/st_tx_audio_session.c | 22 ++++++------ lib/src/st2110/st_tx_fastmetadata_session.c | 16 ++++----- lib/src/st2110/st_tx_video_session.c | 8 ++--- 5 files changed, 47 insertions(+), 47 deletions(-) diff --git a/lib/src/st2110/st_header.h b/lib/src/st2110/st_header.h index 7b923d564..fd6b3b174 100644 --- a/lib/src/st2110/st_header.h +++ b/lib/src/st2110/st_header.h @@ -164,23 +164,23 @@ struct st_frame_trans { /* timing for pacing */ struct st_tx_video_pacing { - double trs; /* in ns for of 2 consecutive packets, T-Frame / N-Packets */ - double tr_offset; /* in ns, tr offset time of each frame */ + long double trs; /* in ns for of 2 consecutive packets, T-Frame / N-Packets */ + long double tr_offset; /* in ns, tr offset time of each frame */ uint32_t vrx; /* packets unit, VRX start value of each frame */ uint32_t warm_pkts; /* packets unit, pkts for RL pacing warm boot */ - double frame_time; /* time of the frame in nanoseconds */ - double frame_time_sampling; /* time of the frame in sampling(90k) */ + long double frame_time; /* time of the frame in nanoseconds */ + long double frame_time_sampling; /* time of the frame in sampling(90k) */ /* in ns, idle time at the end of frame, frame_time - tr_offset - (trs * pkts) */ - double frame_idle_time; - double reactive; + long double frame_idle_time; + long double reactive; float pad_interval; /* padding pkt interval(pkts level) for RL pacing */ uint32_t rl_rtp_offset_ticks; /* RL-only RTP timestamp shift (media-clk ticks) */ uint64_t cur_epochs; /* epoch of current frame */ /* timestamp for rtp header */ uint32_t rtp_time_stamp; - uint64_t tsc_time_cursor; /* in ns, tsc time cursor for packet pacing */ - uint64_t ptp_time_cursor; /* in ns, ptp time cursor for packet pacing */ + long double tsc_time_cursor; /* in ns, tsc time cursor for packet pacing */ + long double ptp_time_cursor; /* in ns, ptp time cursor for packet pacing */ /* ptp time may onward */ uint32_t max_onward_epochs; uint64_t tsc_time_frame_start; /* start tsc time for frame start */ @@ -694,14 +694,14 @@ struct st_rx_video_sessions_mgr { }; struct st_tx_audio_session_pacing { - double trs; /* in ns for of 2 consecutive packets */ - double pkt_time_sampling; /* time of each pkt in sampling */ + long double trs; /* in ns for of 2 consecutive packets */ + long double pkt_time_sampling; /* time of each pkt in sampling */ uint64_t cur_epochs; /* epoch of current pkt */ /* timestamp for rtp header */ uint32_t rtp_time_stamp; - uint64_t ptp_time_cursor; + long double ptp_time_cursor; /* in ns, tsc time cursor for packet pacing */ - uint64_t tsc_time_cursor; + long double tsc_time_cursor; /* ptp time may onward */ uint32_t max_onward_epochs; /* sometimes it may reach tx_audio_session_sync_pacing in a late time */ @@ -1004,13 +1004,13 @@ struct st_rx_audio_sessions_mgr { }; struct st_tx_ancillary_session_pacing { - double frame_time; /* time of the frame in nanoseconds */ - double frame_time_sampling; /* time of the frame in sampling(90k) */ + long double frame_time; /* time of the frame in nanoseconds */ + long double frame_time_sampling; /* time of the frame in sampling(90k) */ uint64_t cur_epochs; /* epoch of current frame */ /* timestamp for rtp header */ uint32_t rtp_time_stamp; - uint64_t ptp_time_cursor; - double tsc_time_cursor; /* in ns, tsc time cursor for packet pacing */ + long double ptp_time_cursor; + long double tsc_time_cursor; /* in ns, tsc time cursor for packet pacing */ /* ptp time may onward */ uint32_t max_onward_epochs; }; @@ -1256,15 +1256,15 @@ struct st_ancillary_transmitter_impl { }; struct st_tx_fastmetadata_session_pacing { - double frame_time; /* time of the frame in nanoseconds */ - double frame_time_sampling; /* time of the frame in sampling(90k) */ + long double frame_time; /* time of the frame in nanoseconds */ + long double frame_time_sampling; /* time of the frame in sampling(90k) */ uint64_t cur_epochs; /* epoch of current frame */ /* timestamp for rtp header */ uint32_t rtp_time_stamp; /* timestamp for pacing */ uint32_t pacing_time_stamp; - uint64_t ptp_time_cursor; - double tsc_time_cursor; /* in ns, tsc time cursor for packet pacing */ + long double ptp_time_cursor; + long double tsc_time_cursor; /* in ns, tsc time cursor for packet pacing */ /* ptp time may onward */ uint32_t max_onward_epochs; }; diff --git a/lib/src/st2110/st_tx_ancillary_session.c b/lib/src/st2110/st_tx_ancillary_session.c index c5322e79c..e5a93e350 100644 --- a/lib/src/st2110/st_tx_ancillary_session.c +++ b/lib/src/st2110/st_tx_ancillary_session.c @@ -268,7 +268,7 @@ static int tx_ancillary_session_init_pacing(struct st_tx_ancillary_session_impl* pacing->max_onward_epochs = (double)(NS_PER_S * 1) / frame_time; /* 1s */ dbg("%s[%02d], max_onward_epochs %u\n", __func__, idx, pacing->max_onward_epochs); - info("%s[%02d], frame_time %f frame_time_sampling %f\n", __func__, idx, + info("%s[%02d], frame_time %Lf frame_time_sampling %Lf\n", __func__, idx, pacing->frame_time, pacing->frame_time_sampling); return 0; } @@ -283,7 +283,7 @@ static int tx_ancillary_session_init_pacing_epoch( static inline uint64_t tx_ancillary_pacing_time( struct st_tx_ancillary_session_pacing* pacing, uint64_t epochs) { - return nextafter(epochs * pacing->frame_time, INFINITY); + return nextafterl((long double)epochs * pacing->frame_time, INFINITY); } static inline __attribute__((unused)) uint32_t tx_ancillary_pacing_time_stamp( @@ -395,8 +395,8 @@ static int tx_ancillary_session_sync_pacing(struct mtl_main_impl* impl, } pacing->ptp_time_cursor = start_time_tai; - pacing->tsc_time_cursor = (double)cur_tsc + (double)time_to_tx_ns; - dbg("%s(%d), epochs %" PRIu64 " ptp_time_cursor %" PRIu64 " time_to_tx_ns %" PRId64 + pacing->tsc_time_cursor = (long double)cur_tsc + (long double)time_to_tx_ns; + dbg("%s(%d), epochs %" PRIu64 " ptp_time_cursor %Lf time_to_tx_ns %" PRId64 "\n", __func__, s->idx, pacing->cur_epochs, pacing->ptp_time_cursor, time_to_tx_ns); diff --git a/lib/src/st2110/st_tx_audio_session.c b/lib/src/st2110/st_tx_audio_session.c index ed069a390..4609a2003 100644 --- a/lib/src/st2110/st_tx_audio_session.c +++ b/lib/src/st2110/st_tx_audio_session.c @@ -215,7 +215,7 @@ static int tx_audio_session_init_pacing(struct st_tx_audio_session_impl* s) { dbg("%s[%02d], max_onward_epochs %u max_late_epochs %u\n", __func__, idx, pacing->max_onward_epochs, pacing->max_late_epochs); - info("%s[%02d], trs %f pkt_time_sampling %f\n", __func__, idx, pacing->trs, + info("%s[%02d], trs %Lf pkt_time_sampling %Lf\n", __func__, idx, pacing->trs, pacing->pkt_time_sampling); return 0; } @@ -228,9 +228,9 @@ static int tx_audio_session_init_pacing_epoch(struct mtl_main_impl* impl, return 0; } -static inline double tx_audio_pacing_time(struct st_tx_audio_session_pacing* pacing, +static inline uint64_t tx_audio_pacing_time(struct st_tx_audio_session_pacing* pacing, uint64_t epochs) { - return epochs * pacing->trs; + return nextafterl((long double)epochs * pacing->trs, INFINITY); } static inline uint32_t tx_audio_pacing_time_stamp( @@ -271,7 +271,7 @@ static int tx_audio_session_sync_pacing(struct mtl_main_impl* impl, uint64_t ptp_time = mt_get_ptp_time(impl, MTL_PORT_P); uint64_t next_epochs = pacing->cur_epochs + 1; uint64_t epochs; - double to_epoch; + int64_t to_epoch; uint64_t ptp_epochs; uint64_t diff; @@ -305,15 +305,15 @@ static int tx_audio_session_sync_pacing(struct mtl_main_impl* impl, } if (required_tai) { - to_epoch = (double)required_tai - ptp_time; - if (to_epoch > NS_PER_S) { + to_epoch = (int64_t)required_tai - (int64_t)ptp_time; + if (to_epoch > (int64_t)NS_PER_S) { dbg("%s(%d), required tai %" PRIu64 " ptp_epochs %" PRIu64 " epochs %" PRIu64 "\n", __func__, s->idx, required_tai, ptp_epochs, epochs); s->port_user_stats.common.stat_error_user_timestamp++; to_epoch = NS_PER_S; // do our best to slow down } } else { - to_epoch = tx_audio_pacing_time(pacing, epochs) - ptp_time; + to_epoch = (int64_t)tx_audio_pacing_time(pacing, epochs) - (int64_t)ptp_time; } if (to_epoch < 0) { @@ -357,12 +357,12 @@ static int tx_audio_session_sync_pacing(struct mtl_main_impl* impl, (rtp_timestamp_delta_us * NS_PER_US) * pacing->pkt_time_sampling / pkt_time; pacing->rtp_time_stamp += rtp_timestamp_delta; } - pacing->tsc_time_cursor = (double)mt_get_tsc(impl) + to_epoch; + pacing->tsc_time_cursor = (long double)mt_get_tsc(impl) + to_epoch; dbg("%s(%d), epochs %" PRIu64 ", rtp_time_stamp %u\n", __func__, s->idx, epochs, pacing->rtp_time_stamp); if (sync) { - dbg("%s(%d), delay to epoch_time %" PRIu64 ", cur %" PRIu64 "\n", __func__, s->idx, + dbg("%s(%d), delay to epoch_time %Lf, cur %" PRIu64 "\n", __func__, s->idx, pacing->tsc_time_cursor, mt_get_tsc(impl)); mt_tsc_delay_to(impl, pacing->tsc_time_cursor); } @@ -2204,9 +2204,9 @@ static int tx_audio_session_attach(struct mtl_main_impl* impl, info("%s(%d), fmt %d channel %u sampling %d ptime %d pt %u\n", __func__, idx, ops->fmt, ops->channel, ops->sampling, ops->ptime, ops->payload_type); - info("%s(%d), pkt_len %u frame_size %u frames %u fps %f, pacing_way %s\n", __func__, + info("%s(%d), pkt_len %u frame_size %u frames %u fps %Lf, pacing_way %s\n", __func__, idx, s->pkt_len, s->st30_frame_size, s->st30_frames_cnt, - (double)NS_PER_S / s->pacing.trs / s->st30_total_pkts, + (long double)NS_PER_S / s->pacing.trs / s->st30_total_pkts, audio_pacing_way_name(s->tx_pacing_way)); return 0; } diff --git a/lib/src/st2110/st_tx_fastmetadata_session.c b/lib/src/st2110/st_tx_fastmetadata_session.c index 808e438a1..5e5f7162d 100644 --- a/lib/src/st2110/st_tx_fastmetadata_session.c +++ b/lib/src/st2110/st_tx_fastmetadata_session.c @@ -211,7 +211,7 @@ static int tx_fastmetadata_session_init_pacing( pacing->max_onward_epochs = (double)(NS_PER_S * 1) / frame_time; /* 1s */ dbg("%s[%02d], max_onward_epochs %u\n", __func__, idx, pacing->max_onward_epochs); - info("%s[%02d], frame_time %f frame_time_sampling %f\n", __func__, idx, + info("%s[%02d], frame_time %Lf frame_time_sampling %Lf\n", __func__, idx, pacing->frame_time, pacing->frame_time_sampling); return 0; } @@ -224,9 +224,9 @@ static int tx_fastmetadata_session_init_pacing_epoch( return 0; } -static inline double tx_fastmetadata_pacing_time( +static inline uint64_t tx_fastmetadata_pacing_time( struct st_tx_fastmetadata_session_pacing* pacing, uint64_t epochs) { - return epochs * pacing->frame_time; + return nextafterl((long double)epochs * pacing->frame_time, INFINITY); } static inline uint32_t tx_fastmetadata_pacing_time_stamp( @@ -267,7 +267,7 @@ static int tx_fastmetadata_session_sync_pacing(struct mtl_main_impl* impl, uint64_t ptp_time = mt_get_ptp_time(impl, MTL_PORT_P); uint64_t next_epochs = pacing->cur_epochs + 1; uint64_t epochs; - double to_epoch; + int64_t to_epoch; bool interlaced = s->ops.interlaced; if (required_tai) { @@ -300,7 +300,7 @@ static int tx_fastmetadata_session_sync_pacing(struct mtl_main_impl* impl, } } - to_epoch = tx_fastmetadata_pacing_time(pacing, epochs) - ptp_time; + to_epoch = (int64_t)tx_fastmetadata_pacing_time(pacing, epochs) - (int64_t)ptp_time; if (to_epoch < 0) { /* time bigger than the assigned epoch time */ s->port_user_stats.common.stat_epoch_mismatch++; @@ -317,13 +317,13 @@ static int tx_fastmetadata_session_sync_pacing(struct mtl_main_impl* impl, pacing->ptp_time_cursor = tx_fastmetadata_pacing_time(pacing, epochs); pacing->pacing_time_stamp = tx_fastmetadata_pacing_time_stamp(pacing, epochs); pacing->rtp_time_stamp = pacing->pacing_time_stamp; - pacing->tsc_time_cursor = (double)mt_get_tsc(impl) + to_epoch; - dbg("%s(%d), epochs %" PRIu64 " time_stamp %u time_cursor %f to_epoch %f\n", __func__, + pacing->tsc_time_cursor = (long double)mt_get_tsc(impl) + to_epoch; + dbg("%s(%d), epochs %" PRIu64 " time_stamp %u time_cursor %Lf to_epoch %" PRId64 "\n", __func__, s->idx, pacing->cur_epochs, pacing->pacing_time_stamp, pacing->tsc_time_cursor, to_epoch); if (sync) { - dbg("%s(%d), delay to epoch_time %f, cur %" PRIu64 "\n", __func__, s->idx, + dbg("%s(%d), delay to epoch_time %Lf, cur %" PRIu64 "\n", __func__, s->idx, pacing->tsc_time_cursor, mt_get_tsc(impl)); mt_tsc_delay_to(impl, pacing->tsc_time_cursor); } diff --git a/lib/src/st2110/st_tx_video_session.c b/lib/src/st2110/st_tx_video_session.c index 3444620d5..7fd006b57 100644 --- a/lib/src/st2110/st_tx_video_session.c +++ b/lib/src/st2110/st_tx_video_session.c @@ -57,7 +57,7 @@ static inline uint64_t tai_from_frame_count(struct st_tx_video_pacing* pacing, * may truncate to a smaller value. Using nextafter(val, INFINITY) ensures we round * up to the next representable double before casting, avoiding jumping between tai in * neighboring frames. This caused problems when tai was again changed to frame count */ - return nextafter(frame_count * pacing->frame_time, INFINITY); + return nextafterl((long double)frame_count * pacing->frame_time, INFINITY); } /* transmission start time of the frame */ @@ -517,9 +517,9 @@ static int tv_init_pacing(struct mtl_main_impl* impl, pacing->trs = frame_time * pacing->reactive / s->st20_total_pkts; pacing->frame_idle_time = frame_time - pacing->tr_offset - frame_time * pacing->reactive; - dbg("%s[%02d], frame_idle_time %f\n", __func__, idx, pacing->frame_idle_time); + dbg("%s[%02d], frame_idle_time %Lf\n", __func__, idx, pacing->frame_idle_time); if (pacing->frame_idle_time < 0) { - warn("%s[%02d], error frame_idle_time %f\n", __func__, idx, pacing->frame_idle_time); + warn("%s[%02d], error frame_idle_time %Lf\n", __func__, idx, pacing->frame_idle_time); pacing->frame_idle_time = 0; } pacing->max_onward_epochs = (double)NS_PER_S / frame_time; /* 1s */ @@ -606,7 +606,7 @@ static int tv_init_pacing(struct mtl_main_impl* impl, info("%s[%02d], wide pacing\n", __func__, idx); } info( - "%s[%02d], trs %f trOffset %f vrx %u warm_pkts %u frame time %fms fps %f " + "%s[%02d], trs %Lf trOffset %Lf vrx %u warm_pkts %u frame time %Lfms fps %f " "rl_rtp_adj %u ticks\n", __func__, idx, pacing->trs, pacing->tr_offset, pacing->vrx, pacing->warm_pkts, pacing->frame_time / NS_PER_MS, st_frame_rate(s->ops.fps), From 7059fbc3fc040213c1cc9ef019471a2702d4da70 Mon Sep 17 00:00:00 2001 From: "Kasiewicz, Marek" Date: Mon, 29 Jun 2026 07:59:06 +0000 Subject: [PATCH 02/19] Fix: Hardon async t3 delay req timestamp acquisition and PTP tests The out-of-tree ICE driver's single hardware TX timestamp slot can delay t3 availability or latch older timestamps if they're not drained. This forces the async polling path to be resilient against stale timestamps and racing completion. Stiffened the async polling path: - Introduced a lock-free claim step (rte_atomic32_t) to ensure exactly-once delta result processing when t3 or t4 asynchronously complete. - Set the polling retry interval to 200us (up from 50us) to significantly reduce the PMD call frequency during heavy TX loads, lowering logging pressure when the hardware slot is busy. - Added strict sequence-id check to verify t3 belongs to the current measurement cycle, plus an egress guard ensuring the reading exceeds the live t2 value to ignore stale buffered logs. - Downgraded the recoverable single-cycle sync timeout status log level from error to warn. - Corrected unit tests in tests/unit/ptp/ to accurately emulate and validate the correct clock synchronization behavior, ensuring robust and non-overfitted test execution. Signed-off-by: Kasiewicz, Marek --- lib/src/mt_main.h | 18 +-- lib/src/mt_ptp.c | 159 ++++++++++---------- lib/src/mt_ptp.h | 10 ++ tests/unit/main.cpp | 3 +- tests/unit/meson.build | 17 ++- tests/unit/ptp/adjust_delta_test.cpp | 67 +++++++++ tests/unit/ptp/delta_math_test.cpp | 71 +++++++++ tests/unit/ptp/ptp_harness.c | 213 +++++++++++++++++++++++++++ tests/unit/ptp/ptp_harness.h | 103 +++++++++++++ tests/unit/ptp/servo_test.cpp | 141 ++++++++++++++++++ tests/unit/ptp/t3_test.cpp | 110 ++++++++++++++ 11 files changed, 823 insertions(+), 89 deletions(-) create mode 100644 tests/unit/ptp/adjust_delta_test.cpp create mode 100644 tests/unit/ptp/delta_math_test.cpp create mode 100644 tests/unit/ptp/ptp_harness.c create mode 100644 tests/unit/ptp/ptp_harness.h create mode 100644 tests/unit/ptp/servo_test.cpp create mode 100644 tests/unit/ptp/t3_test.cpp diff --git a/lib/src/mt_main.h b/lib/src/mt_main.h index 7ba0ed0e2..368d50a1b 100644 --- a/lib/src/mt_main.h +++ b/lib/src/mt_main.h @@ -200,15 +200,6 @@ struct mt_ptp_impl { uint16_t port_id; bool active; /* if the ptp stack is running */ bool no_timesync; - /* - * The flag indicates Qbv (IEEE 802.1Qbv) traffic shaper - * enable. - * - * The IEEE 802.1Qbv is designed to separate traffics - * transmission into different time slices to prevent - * traffics transmission interfering. - */ - bool qbv_enabled; int64_t no_timesync_delta; /* for no cni case */ @@ -243,6 +234,14 @@ struct mt_ptp_impl { uint16_t t3_sequence_id; uint64_t t4; + /* async read state for the delay_req tx timestamp (t3) */ + bool t3_pending; + uint16_t t3_pending_seq; + uint64_t t3_deadline_ns; + /* single-owner claim so t3 and t4 completing on two threads apply the + * cycle's result exactly once; reset per cycle in ptp_parse_sync */ + rte_atomic32_t result_claimed; + bool calibrate_t2_t3; /* for no_timesync case which the t2 and t3 get from tsc */ bool locked; @@ -303,6 +302,7 @@ struct mt_ptp_impl { int32_t stat_sync_cnt; int32_t stat_t2_t1_delta_calibrate; int32_t stat_t4_t3_delta_calibrate; + int32_t stat_t3_timeout; uint16_t stat_sync_keep; }; diff --git a/lib/src/mt_ptp.c b/lib/src/mt_ptp.c index c0785c9c2..b12edc414 100644 --- a/lib/src/mt_ptp.c +++ b/lib/src/mt_ptp.c @@ -554,19 +554,6 @@ static void ptp_adjust_delta(struct mt_ptp_impl* ptp, int64_t delta, bool error_ } } -static void ptp_delay_req_read_tx_time_handler(void* param) { - struct mt_ptp_impl* ptp = param; - uint64_t tx_ns = 0; - int ret; - - ret = ptp_timesync_read_tx_time(ptp, &tx_ns); - if (ret >= 0) { - ptp->t3 = tx_ns; - } else { - if (!ptp->t4) rte_eal_alarm_set(5, ptp_delay_req_read_tx_time_handler, ptp); - } -} - static void ptp_expect_result_clear(struct mt_ptp_impl* ptp) { ptp->expect_result_cnt = 0; ptp->expect_result_sum = 0; @@ -809,11 +796,56 @@ static int ptp_parse_result(struct mt_ptp_impl* ptp) { return 0; } +/* t3 (alarm thread) and t4 (rx thread) can both observe a full cycle; the + * single-owner claim lets exactly one consume it so the delta applies once. + * result_claimed is reset per cycle in ptp_parse_sync. */ +static void ptp_try_complete_result(struct mt_ptp_impl* ptp) { + if (!(ptp->t1 && ptp->t2 && ptp->t3 && ptp->t4)) return; + if (rte_atomic32_test_and_set(&ptp->result_claimed)) ptp_parse_result(ptp); +} + +static void ptp_delay_req_read_tx_time_handler(void* param) { + struct mt_ptp_impl* ptp = param; + uint64_t tx_ns = 0; + int ret; + + if (!ptp->t3_pending) return; /* no outstanding delay_req tx timestamp */ + + /* a newer delay_req has been sent, abandon this stale read */ + if (ptp->t3_sequence_id != ptp->t3_pending_seq) { + ptp->t3_pending = false; + return; + } + + ret = ptp_timesync_read_tx_time(ptp, &tx_ns); + /* A valid t3 is the local-clock egress time of a delay_req sent after this + * cycle's sync, so it must exceed t2; a smaller value is a stale timestamp + * latched by an earlier cycle and now drained, so keep polling for the real + * one rather than feeding the stale value into the result before lock. */ + if (ret >= 0 && tx_ns > ptp->t2) { + ptp->t3 = tx_ns; + ptp->t3_pending = false; + MT_USDT_PTP_MSG(ptp->port, 3, ptp->t3); + ptp_try_complete_result(ptp); + return; + } + + if (mt_get_monotonic_time() < ptp->t3_deadline_ns) { + rte_eal_alarm_set(MT_PTP_DELAY_REQ_TX_TS_RETRY_US, ptp_delay_req_read_tx_time_handler, + ptp); + } else { + /* give up on this cycle and drop the partial result so the next sync is clean */ + ptp->t3_pending = false; + ptp->stat_t3_timeout++; + ptp_t_result_clear(ptp); + ptp_expect_result_clear(ptp); + } +} + static void ptp_delay_req_task(struct mt_ptp_impl* ptp) { enum mtl_port port = ptp->port; size_t hdr_offset; struct mt_ptp_sync_msg* msg; - uint64_t tx_ns = 0; if (ptp->t3) return; /* t3 already sent */ @@ -878,65 +910,35 @@ static void ptp_delay_req_task(struct mt_ptp_impl* ptp) { err("%s(%d), tx fail\n", __func__, port); return; } -#if MT_PTP_CHECK_HW_SW_DELTA - uint64_t burst_time = ptp_get_raw_time(ptp); -#endif #if MT_PTP_USE_TX_TIME_STAMP - if (ptp->qbv_enabled) { - /* - * The DELAY_REQ packet will be blocked max 1.2ms by Qbv scheduler. - * The Tx timestamp will not be created immediately. So, start an - * alarm task to poll the Tx timestamp. - */ - rte_eal_alarm_set(5, ptp_delay_req_read_tx_time_handler, ptp); - } else { - /* Wait max 50 us to read TX timestamp. */ - int max_retry = 50; - int ret; - - while (max_retry > 0) { - ret = ptp_timesync_read_tx_time(ptp, &tx_ns); - if (ret >= 0) { -#if MT_PTP_CHECK_HW_SW_DELTA - info("%s(%d), t3 hw-sw delta %" PRId64 "\n", __func__, ptp->port, - tx_ns - burst_time); -#endif - break; - } - - mt_delay_us(1); - max_retry--; - } - - if (max_retry <= 0) { - err("%s(%d), read tx reach max retry\n", __func__, port); - } - -#if MT_PTP_CHECK_TX_TIME_STAMP - uint64_t ptp_ns = ptp_timesync_read_time(ptp); - uint64_t delta = ptp_ns - tx_ns; -#define TX_MAX_DELTA (1 * 1000 * 1000) /* 1ms */ - if (unlikely(delta > TX_MAX_DELTA)) { - err("%s(%d), tx_ns %" PRIu64 ", delta %" PRIu64 "\n", __func__, ptp->port, tx_ns, - delta); - ptp->stat_tx_sync_err++; - } -#endif - - ptp->t3 = tx_ns; + /* + * The PHY does not produce the tx timestamp until the DELAY_REQ has actually + * egressed, which can be delayed by tx queue backlog or the Qbv scheduler (up + * to ~1.2ms). Poll it asynchronously via an alarm rather than busy-waiting + * here. The deadline must stay well below the minimum PTP sync interval: t3 + * stays 0 for the whole async window, so the single-outstanding-request + * invariant holds only if the read finishes before the next sync. The seq + * guard rejects stale alarms but not stale HW-slot contents (see the t3 > t2 + * check in the read handler). + */ + ptp->t3_pending = true; + ptp->t3_pending_seq = ptp->t3_sequence_id; + ptp->t3_deadline_ns = + mt_get_monotonic_time() + (uint64_t)MT_PTP_DELAY_REQ_TX_TS_DEADLINE_US * 1000; + rte_eal_alarm_set(MT_PTP_DELAY_REQ_TX_TS_RETRY_US, ptp_delay_req_read_tx_time_handler, + ptp); #else ptp->t3 = ptp_get_raw_time(ptp); -#endif - dbg("%s(%d), t3 %" PRIu64 ", seq %d, max_retry %d, ptp %" PRIu64 "\n", __func__, port, - ptp->t3, ptp->t3_sequence_id, max_retry, ptp_get_raw_time(ptp)); - MT_USDT_PTP_MSG(ptp->port, 3, ptp->t3); + dbg("%s(%d), t3 %" PRIu64 ", seq %d, ptp %" PRIu64 "\n", __func__, port, ptp->t3, + ptp->t3_sequence_id, ptp_get_raw_time(ptp)); + MT_USDT_PTP_MSG(ptp->port, 3, ptp->t3); - /* all time get */ - if (ptp->t4 && ptp->t2 && ptp->t1) { - ptp_parse_result(ptp); - } + /* all time get */ + if (ptp->t4 && ptp->t2 && ptp->t1) { + ptp_parse_result(ptp); } +#endif } #if MT_PTP_USE_TX_TIMER @@ -986,8 +988,13 @@ static int ptp_parse_sync(struct mt_ptp_impl* ptp, struct mt_ptp_sync_msg* msg, #if MT_PTP_USE_TX_TIMER rte_eal_alarm_cancel(ptp_delay_req_handler, ptp); +#endif +#if MT_PTP_USE_TX_TIME_STAMP + rte_eal_alarm_cancel(ptp_delay_req_read_tx_time_handler, ptp); + ptp->t3_pending = false; #endif ptp_t_result_clear(ptp); + rte_atomic32_clear(&ptp->result_claimed); /* start a new measurement cycle */ ptp->t2 = rx_ns; ptp->t2_sequence_id = msg->hdr.sequence_id; ptp->t2_vlan = vlan; @@ -1088,10 +1095,7 @@ static int ptp_parse_delay_resp(struct mt_ptp_impl* ptp, ptp->t3_sequence_id, ptp_get_raw_time(ptp)); MT_USDT_PTP_MSG(ptp->port, 4, ptp->t4); - /* all time get */ - if (ptp->t3 && ptp->t2 && ptp->t1) { - ptp_parse_result(ptp); - } + ptp_try_complete_result(ptp); return 0; } @@ -1270,7 +1274,6 @@ static int ptp_init(struct mtl_main_impl* impl, struct mt_ptp_impl* ptp, struct rte_ether_addr mac; int ret; uint8_t* ip = &ptp->sip_addr[0]; - struct mt_interface* inf = mt_if(impl, port); ret = mt_macaddr_get(impl, port, &mac); if (ret < 0) { @@ -1310,8 +1313,6 @@ static int ptp_init(struct mtl_main_impl* impl, struct mt_ptp_impl* ptp, } else { ptp->master_addr_mode = MT_PTP_MULTICAST_ADDR; } - ptp->qbv_enabled = - ((ST21_TX_PACING_WAY_TSN == p->pacing) && (MT_DRV_IGC == inf->drv_info.drv_type)); ptp->locked = false; ptp->stat_sync_keep = 0; @@ -1403,8 +1404,8 @@ static int ptp_uinit(struct mtl_main_impl* impl, struct mt_ptp_impl* ptp) { #endif rte_eal_alarm_cancel(ptp_sync_timeout_handler, ptp); rte_eal_alarm_cancel(ptp_monitor_handler, ptp); -#ifdef MT_PTP_USE_TX_TIME_STAMP - if (ptp->qbv_enabled) rte_eal_alarm_cancel(ptp_delay_req_read_tx_time_handler, ptp); +#if MT_PTP_USE_TX_TIME_STAMP + rte_eal_alarm_cancel(ptp_delay_req_read_tx_time_handler, ptp); #endif if (!ptp->active) return 0; @@ -1534,7 +1535,7 @@ static int ptp_stat(void* priv) { notice("PTP(%d): rx time error %d, tx time error %d, delta result error %d\n", port, ptp->stat_rx_sync_err, ptp->stat_tx_sync_err, ptp->stat_result_err); if (ptp->stat_sync_timeout_err) - err("PTP(%d): sync timeout %d\n", port, ptp->stat_sync_timeout_err); + warn("PTP(%d): sync timeout %d\n", port, ptp->stat_sync_timeout_err); if (ptp->calibrate_t2_t3) { notice("PTP(%d): t2_t1_delta_calibrate %d t4_t3_delta_calibrate %d\n", port, @@ -1546,6 +1547,10 @@ static int ptp_stat(void* priv) { err("PTP(%d): t3 sequence id mismatch %d\n", port, ptp->stat_t3_sequence_id_mismatch); ptp->stat_t3_sequence_id_mismatch = 0; } + if (ptp->stat_t3_timeout) { + err("PTP(%d): t3 tx timestamp timeout %d\n", port, ptp->stat_t3_timeout); + ptp->stat_t3_timeout = 0; + } ptp_stat_clear(ptp); diff --git a/lib/src/mt_ptp.h b/lib/src/mt_ptp.h index d985b5d81..a8a620261 100644 --- a/lib/src/mt_ptp.h +++ b/lib/src/mt_ptp.h @@ -10,6 +10,16 @@ #define MT_PTP_DELAY_REQ_US (50) #define MT_PTP_DELAY_REQ_MONITOR_US (1000 * 1) #define MT_PTP_DELAY_STEP_US (10) +/* + * Poll interval and total budget for the async delay_req tx timestamp (t3) read. + * Each failed read makes the PMD busy-wait ~10us and log one ERR, so the 200us + * interval caps a fully delayed cycle at ~10 polls (~100us busy-wait, <=10 ERR + * lines) instead of ~40 at 50us. The added latency on the common path is at most + * one interval, negligible against the sync interval. The 2000us budget still + * covers the ~1.2ms Qbv egress worst case with margin. + */ +#define MT_PTP_DELAY_REQ_TX_TS_RETRY_US (200) +#define MT_PTP_DELAY_REQ_TX_TS_DEADLINE_US (2000) #define MT_PTP_CLOCK_IDENTITY_MAGIC (0xfeff) #define MT_PTP_STAT_INTERVAL_S (10) /* 10s */ diff --git a/tests/unit/main.cpp b/tests/unit/main.cpp index 03b0edd53..986c13be9 100644 --- a/tests/unit/main.cpp +++ b/tests/unit/main.cpp @@ -2,9 +2,10 @@ * Copyright(c) 2025 Intel Corporation */ +#include #include int main(int argc, char** argv) { - ::testing::InitGoogleTest(&argc, argv); + ::testing::InitGoogleMock(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/tests/unit/meson.build b/tests/unit/meson.build index 0d0554835..ffe477dd1 100644 --- a/tests/unit/meson.build +++ b/tests/unit/meson.build @@ -13,8 +13,16 @@ unit_include_dirs = [ ] gtest_dep = dependency('gtest', required : true) +gmock_dep = dependency('gmock', required : true) libpthread_dep = meson.get_compiler('c').find_library('pthread', required : true) +# When GPU-direct is enabled, the included production .c files reference +# gpu_allocate_shared_buffer; link the gpu_direct DSO so --no-undefined passes. +unit_extra_deps = [] +if gpu_direct_dep.found() + unit_extra_deps += gpu_direct_dep +endif + unit_sources = [ 'common/ut_common.c', 'session/st40_harness.c', @@ -70,6 +78,11 @@ unit_sources = [ 'pipeline/st22p_tx_harness.c', 'pipeline/st22p_concurrency_test.cpp', 'pipeline/st22p_concurrency_stress_test.cpp', + 'ptp/ptp_harness.c', + 'ptp/servo_test.cpp', + 'ptp/adjust_delta_test.cpp', + 'ptp/delta_math_test.cpp', + 'ptp/t3_test.cpp', 'main.cpp', ] @@ -78,6 +91,6 @@ executable('UnitTest', unit_sources, cpp_args : unit_cpp_args, link_args : unit_link_args, include_directories : unit_include_dirs, - dependencies : [mtl_internal_dep, dpdk_dep, libm_dep, gtest_dep, libpthread_dep, - usdt_provider_header_dep], + dependencies : [mtl_internal_dep, dpdk_dep, libm_dep, gtest_dep, gmock_dep, libpthread_dep, + usdt_provider_header_dep] + unit_extra_deps, install : false) diff --git a/tests/unit/ptp/adjust_delta_test.cpp b/tests/unit/ptp/adjust_delta_test.cpp new file mode 100644 index 000000000..97d261548 --- /dev/null +++ b/tests/unit/ptp/adjust_delta_test.cpp @@ -0,0 +1,67 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * Pins the delta bookkeeping and `locked` detection in the tail of + * `ptp_adjust_delta`. With no_timesync=true every PHC adjustment routes into + * the no_timesync_delta accumulator, so the bookkeeping runs with no NIC. + * + * Build: meson setup build_unit -Denable_unit_tests=true && ninja -C build_unit + * Run: ./build_unit/tests/unit/UnitTest --gtest_filter='PtpAdjustDelta*' + */ + +#include + +#include "ptp/ptp_harness.h" + +class PtpAdjustDeltaTest : public ::testing::Test { + protected: + void SetUp() override { + ASSERT_EQ(ut_ptp_init(), 0); + ctx_ = ut_ptp_create(); + ASSERT_NE(ctx_, nullptr); + } + void TearDown() override { + ut_ptp_destroy(ctx_); + } + ut_ptp_ctx* ctx_ = nullptr; +}; + +/* ptp_delta is the signed running sum; stat_delta_min/max track signed + * extremes; stat_delta_cnt and delta_result_cnt count every call. */ +TEST_F(PtpAdjustDeltaTest, SignedDeltaBookkeeping) { + const int64_t deltas[] = {50, -30, 0, 1000000}; + for (int64_t d : deltas) ut_ptp_adjust_delta(ctx_, d, false); + + EXPECT_EQ(ut_ptp_ptp_delta(ctx_), 50 - 30 + 0 + 1000000); + EXPECT_EQ(ut_ptp_stat_delta_max(ctx_), 1000000); + EXPECT_EQ(ut_ptp_stat_delta_min(ctx_), -30); + EXPECT_EQ(ut_ptp_stat_delta_cnt(ctx_), 4); + EXPECT_EQ(ut_ptp_delta_result_cnt(ctx_), 4u); + /* under no_timesync the same signed sum lands in the accumulator */ + EXPECT_EQ(ut_ptp_no_timesync_delta(ctx_), 50 - 30 + 0 + 1000000); +} + +/* `locked` requires BOTH stat_delta_min and stat_delta_max within (0,100) in + * absolute value -- a small negative AND a small positive delta -- sustained + * past the stat_sync_keep > 100 threshold. */ +TEST_F(PtpAdjustDeltaTest, LockedAfterSustainedSmallDeltas) { + ut_ptp_adjust_delta(ctx_, 50, false); /* max=50, min=0 */ + ut_ptp_adjust_delta(ctx_, -50, false); /* max=50, min=-50 */ + EXPECT_FALSE(ut_ptp_locked(ctx_)); + + for (int i = 0; i < 210; i++) ut_ptp_adjust_delta(ctx_, 50, false); + EXPECT_TRUE(ut_ptp_locked(ctx_)); +} + +/* A delta >= 100ns pushes stat_delta_max out of range, resetting the keep + * counter and preventing lock. */ +TEST_F(PtpAdjustDeltaTest, LargeDeltaResetsSyncKeep) { + ut_ptp_adjust_delta(ctx_, 50, false); + ut_ptp_adjust_delta(ctx_, -50, false); + for (int i = 0; i < 5; i++) ut_ptp_adjust_delta(ctx_, 50, false); + EXPECT_GT(ut_ptp_stat_sync_keep(ctx_), 0); + + ut_ptp_adjust_delta(ctx_, 200, false); /* max becomes 200 -> out of range */ + EXPECT_EQ(ut_ptp_stat_sync_keep(ctx_), 0); + EXPECT_FALSE(ut_ptp_locked(ctx_)); +} diff --git a/tests/unit/ptp/delta_math_test.cpp b/tests/unit/ptp/delta_math_test.cpp new file mode 100644 index 000000000..378739d5f --- /dev/null +++ b/tests/unit/ptp/delta_math_test.cpp @@ -0,0 +1,71 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * Pure-function pins: `ptp_correct_ts` (coefficient scaling about the last + * sync point) and `ptp_net_tmstamp_to_ns` (48-bit network-order seconds + + * nanoseconds). + * + * Build: meson setup build_unit -Denable_unit_tests=true && ninja -C build_unit + * Run: ./build_unit/tests/unit/UnitTest --gtest_filter='PtpDeltaMath*' + */ + +#include + +#include "ptp/ptp_harness.h" + +static constexpr uint64_t kNsPerS = 1000000000ULL; + +class PtpDeltaMathTest : public ::testing::Test { + protected: + void SetUp() override { + ASSERT_EQ(ut_ptp_init(), 0); + ctx_ = ut_ptp_create(); + ASSERT_NE(ctx_, nullptr); + } + void TearDown() override { + ut_ptp_destroy(ctx_); + } + ut_ptp_ctx* ctx_ = nullptr; +}; + +/* coefficient 1.0 is the identity transform regardless of last_sync_ts. */ +TEST_F(PtpDeltaMathTest, CorrectTsIdentityAtUnitCoefficient) { + ut_ptp_set_coefficient(ctx_, 1.0); + ut_ptp_set_last_sync_ts(ctx_, 1000); + EXPECT_EQ(ut_ptp_correct_ts(ctx_, 2000), 2000u); + /* ts before last_sync_ts -> negative advance, still identity */ + EXPECT_EQ(ut_ptp_correct_ts(ctx_, 500), 500u); +} + +/* coefficient != 1.0 scales the advance from last_sync_ts. */ +TEST_F(PtpDeltaMathTest, CorrectTsScalesAdvance) { + ut_ptp_set_coefficient(ctx_, 1.5); + ut_ptp_set_last_sync_ts(ctx_, 1000); + /* advance = 1000, scaled = 1500 -> 2500 */ + EXPECT_EQ(ut_ptp_correct_ts(ctx_, 2000), 2500u); +} + +/* Negative advance with a coefficient applied. */ +TEST_F(PtpDeltaMathTest, CorrectTsScalesNegativeAdvance) { + ut_ptp_set_coefficient(ctx_, 2.0); + ut_ptp_set_last_sync_ts(ctx_, 2000); + /* advance = -1000, scaled = -2000 -> 0 */ + EXPECT_EQ(ut_ptp_correct_ts(ctx_, 1000), 0u); +} + +/* network-order seconds (msb<<32 | lsb) * 1e9 + ns. */ +TEST_F(PtpDeltaMathTest, NetTmstampBasic) { + EXPECT_EQ(ut_ptp_net_tmstamp_to_ns(0, 5, 123), 5ULL * kNsPerS + 123); +} + +/* 48-bit seconds boundary uses both the msb and lsb words. */ +TEST_F(PtpDeltaMathTest, NetTmstamp48BitBoundary) { + const uint64_t sec = (uint64_t)0xFFFFULL << 32 | 0xFFFFFFFFULL; /* 2^48 - 1 */ + EXPECT_EQ(ut_ptp_net_tmstamp_to_ns(0xFFFF, 0xFFFFFFFF, 0), sec * kNsPerS); +} + +/* msb contributes the high 16 bits of the 48-bit seconds field. */ +TEST_F(PtpDeltaMathTest, NetTmstampMsbContribution) { + const uint64_t sec = (uint64_t)0x0001ULL << 32 | 0x00000002ULL; + EXPECT_EQ(ut_ptp_net_tmstamp_to_ns(0x0001, 0x00000002, 7), sec * kNsPerS + 7); +} diff --git a/tests/unit/ptp/ptp_harness.c b/tests/unit/ptp/ptp_harness.c new file mode 100644 index 000000000..e6df100bc --- /dev/null +++ b/tests/unit/ptp/ptp_harness.c @@ -0,0 +1,213 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * C harness for the PTP servo / delta-adjustment unit tests. + * + * Includes the production mt_ptp.c directly so that the file-local static + * functions (pi_sample, ptp_adjust_delta, ptp_correct_ts, + * ptp_net_tmstamp_to_ns, ...) become visible in this translation unit. + * Non-static symbols duplicate those in libmtl; --allow-multiple-definition + * resolves this. USDT is disabled to avoid probe-semaphore link references. + */ + +/* mt_main.h defines _GNU_SOURCE, but ut_common.h pulls system headers first; + * define it here so struct timex / clock_adjtime are exposed to mt_ptp.c. */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#include +#include +#include + +#undef MTL_HAS_USDT +#include "common/ut_common.h" +#include "mt_ptp.c" + +/* ── opaque context ───────────────────────────────────────────────────── */ + +struct ut_ptp_ctx { + struct mtl_main_impl impl; + struct mt_ptp_impl ptp; +}; + +/* pull in the public header after the struct so the opaque typedef resolves */ +#include "ptp/ptp_harness.h" + +/* The rte_eth_timesync_* calls reached when no_timesync=false are routed to the + * overrides below; -Wl,--allow-multiple-definition lets them win over libdpdk. */ +static int ut_g_read_tx_ret = 0; +static uint64_t ut_g_read_tx_ns = 0; + +int rte_eth_timesync_read_tx_timestamp(uint16_t port_id, struct timespec* timestamp) { + (void)port_id; + if (timestamp) { + timestamp->tv_sec = ut_g_read_tx_ns / NS_PER_S; + timestamp->tv_nsec = ut_g_read_tx_ns % NS_PER_S; + } + return ut_g_read_tx_ret; +} + +void ut_ptp_set_read_tx_ret(int ret) { + ut_g_read_tx_ret = ret; +} + +void ut_ptp_set_read_tx_ns(uint64_t ns) { + ut_g_read_tx_ns = ns; +} + +/* ── init ─────────────────────────────────────────────────────────────── */ + +int ut_ptp_init(void) { + return ut_eal_init(); +} + +/* ── create / destroy ─────────────────────────────────────────────────── */ + +ut_ptp_ctx* ut_ptp_create(void) { + ut_ptp_ctx* ctx = calloc(1, sizeof(*ctx)); + if (!ctx) return NULL; + + ctx->impl.type = MT_HANDLE_MAIN; + ctx->impl.tsc_hz = rte_get_tsc_hz(); + + ctx->ptp.impl = &ctx->impl; + ctx->ptp.port = MTL_PORT_P; + ctx->ptp.port_id = 0; + ctx->ptp.no_timesync = true; + ctx->ptp.coefficient = 1.0; + + /* feature bit lives on the per-interface descriptor reached via mt_if() */ + ctx->impl.inf[MTL_PORT_P].feature = 0; + return ctx; +} + +void ut_ptp_destroy(ut_ptp_ctx* ctx) { + free(ctx); +} + +/* ── setters ──────────────────────────────────────────────────────────── */ + +void ut_ptp_set_phc2sys_active(ut_ptp_ctx* ctx, bool active) { + ctx->ptp.phc2sys_active = active; +} + +void ut_ptp_set_t1(ut_ptp_ctx* ctx, uint64_t t1) { + ctx->ptp.t1 = t1; +} + +void ut_ptp_set_t2(ut_ptp_ctx* ctx, uint64_t t2) { + ctx->ptp.t2 = t2; +} + +void ut_ptp_set_t3(ut_ptp_ctx* ctx, uint64_t t3) { + ctx->ptp.t3 = t3; +} + +void ut_ptp_set_t4(ut_ptp_ctx* ctx, uint64_t t4) { + ctx->ptp.t4 = t4; +} + +void ut_ptp_set_t3_sequence_id(ut_ptp_ctx* ctx, uint16_t seq) { + ctx->ptp.t3_sequence_id = seq; +} + +void ut_ptp_set_t3_pending(ut_ptp_ctx* ctx, bool pending, uint16_t pending_seq) { + ctx->ptp.t3_pending = pending; + ctx->ptp.t3_pending_seq = pending_seq; +} + +void ut_ptp_set_t3_deadline_ns(ut_ptp_ctx* ctx, uint64_t deadline_ns) { + ctx->ptp.t3_deadline_ns = deadline_ns; +} + +void ut_ptp_set_no_timesync(ut_ptp_ctx* ctx, bool no_timesync) { + ctx->ptp.no_timesync = no_timesync; +} + +void ut_ptp_set_coefficient(ut_ptp_ctx* ctx, double coefficient) { + ctx->ptp.coefficient = coefficient; +} + +void ut_ptp_set_last_sync_ts(ut_ptp_ctx* ctx, uint64_t last_sync_ts) { + ctx->ptp.last_sync_ts = last_sync_ts; +} + +/* ── code under test ──────────────────────────────────────────────────── */ + +void ut_ptp_adjust_delta(ut_ptp_ctx* ctx, int64_t delta, bool error_correct) { + ptp_adjust_delta(&ctx->ptp, delta, error_correct); +} + +void ut_ptp_run_t3_handler(ut_ptp_ctx* ctx) { + ptp_delay_req_read_tx_time_handler(&ctx->ptp); +} + +void ut_ptp_try_complete_result(ut_ptp_ctx* ctx) { + ptp_try_complete_result(&ctx->ptp); +} + +double ut_pi_sample(ut_ptp_ctx* ctx, double offset, double local_ts, int* out_state) { + enum servo_state state = UNLOCKED; + double ppb = pi_sample(&ctx->ptp.servo, offset, local_ts, &state); + if (out_state) *out_state = (int)state; + return ppb; +} + +uint64_t ut_ptp_correct_ts(ut_ptp_ctx* ctx, uint64_t ts) { + return ptp_correct_ts(&ctx->ptp, ts); +} + +uint64_t ut_ptp_net_tmstamp_to_ns(uint16_t sec_msb, uint32_t sec_lsb, uint32_t ns) { + struct mt_ptp_tmstamp ts; + ts.sec_msb = htons(sec_msb); + ts.sec_lsb = htonl(sec_lsb); + ts.ns = htonl(ns); + return ptp_net_tmstamp_to_ns(&ts); +} + +/* ── getters ──────────────────────────────────────────────────────────── */ + +int64_t ut_ptp_no_timesync_delta(const ut_ptp_ctx* ctx) { + return ctx->ptp.no_timesync_delta; +} +int64_t ut_ptp_ptp_delta(const ut_ptp_ctx* ctx) { + return ctx->ptp.ptp_delta; +} +int64_t ut_ptp_stat_delta_min(const ut_ptp_ctx* ctx) { + return ctx->ptp.stat_delta_min; +} +int64_t ut_ptp_stat_delta_max(const ut_ptp_ctx* ctx) { + return ctx->ptp.stat_delta_max; +} +int32_t ut_ptp_stat_delta_cnt(const ut_ptp_ctx* ctx) { + return ctx->ptp.stat_delta_cnt; +} +uint64_t ut_ptp_delta_result_cnt(const ut_ptp_ctx* ctx) { + return ctx->ptp.delta_result_cnt; +} +bool ut_ptp_locked(const ut_ptp_ctx* ctx) { + return ctx->ptp.locked; +} +uint16_t ut_ptp_stat_sync_keep(const ut_ptp_ctx* ctx) { + return ctx->ptp.stat_sync_keep; +} +int ut_ptp_servo_count(const ut_ptp_ctx* ctx) { + return ctx->ptp.servo.count; +} +double ut_ptp_servo_drift(const ut_ptp_ctx* ctx) { + return ctx->ptp.servo.drift; +} +uint64_t ut_ptp_t3(const ut_ptp_ctx* ctx) { + return ctx->ptp.t3; +} +bool ut_ptp_t3_pending(const ut_ptp_ctx* ctx) { + return ctx->ptp.t3_pending; +} +int32_t ut_ptp_stat_t3_timeout(const ut_ptp_ctx* ctx) { + return ctx->ptp.stat_t3_timeout; +} +int32_t ut_ptp_result_claimed(const ut_ptp_ctx* ctx) { + return rte_atomic32_read(&ctx->ptp.result_claimed); +} diff --git a/tests/unit/ptp/ptp_harness.h b/tests/unit/ptp/ptp_harness.h new file mode 100644 index 000000000..0aab31583 --- /dev/null +++ b/tests/unit/ptp/ptp_harness.h @@ -0,0 +1,103 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * C harness API for the PTP servo / delta-adjustment unit tests. + * + * The harness wraps a single `struct mt_ptp_impl` driven entirely + * in-process: `no_timesync` is forced true so every `rte_eth_timesync_*` + * call is bypassed and replaced by the `no_timesync_delta` accumulator, + * letting the PI servo and delta bookkeeping run with no NIC, no + * hugepages, and no real PHC. + * + * Only an opaque typedef plus plain C declarations are exposed here so + * the header is safe to include from the C++ gtest translation units; no + * MTL-internal header is pulled in. + */ + +#ifndef _UT_PTP_HARNESS_H_ +#define _UT_PTP_HARNESS_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Mirror of the production `enum servo_state` ordinals (UNLOCKED=0, + * JUMP=1, LOCKED=2) so the C++ tests can assert state transitions + * without including the internal enum. */ +#define UT_PTP_SERVO_UNLOCKED 0 +#define UT_PTP_SERVO_JUMP 1 +#define UT_PTP_SERVO_LOCKED 2 + +typedef struct ut_ptp_ctx ut_ptp_ctx; + +/* Initialise the shared DPDK EAL (idempotent). Returns 0 on success. */ +int ut_ptp_init(void); + +/* Create a zeroed PTP context (no_timesync=true, coefficient=1.0, + * feature bit clear, phc2sys inactive). Caller frees with ut_ptp_destroy. */ +ut_ptp_ctx* ut_ptp_create(void); +void ut_ptp_destroy(ut_ptp_ctx* ctx); + +/* ── setters ──────────────────────────────────────────────────────────── */ +void ut_ptp_set_phc2sys_active(ut_ptp_ctx* ctx, bool active); +void ut_ptp_set_t1(ut_ptp_ctx* ctx, uint64_t t1); +void ut_ptp_set_t2(ut_ptp_ctx* ctx, uint64_t t2); +void ut_ptp_set_t3(ut_ptp_ctx* ctx, uint64_t t3); +void ut_ptp_set_t4(ut_ptp_ctx* ctx, uint64_t t4); +/* Drive the async t3 read state directly. */ +void ut_ptp_set_t3_sequence_id(ut_ptp_ctx* ctx, uint16_t seq); +void ut_ptp_set_t3_pending(ut_ptp_ctx* ctx, bool pending, uint16_t pending_seq); +void ut_ptp_set_t3_deadline_ns(ut_ptp_ctx* ctx, uint64_t deadline_ns); +/* Return value forced from the mocked rte_eth_timesync_read_tx_timestamp when + * no_timesync=false (default 0). Negative simulates a not-yet-ready tx ts. */ +void ut_ptp_set_read_tx_ret(int ret); +/* Timestamp (ns) the mocked read returns on success (default 0). */ +void ut_ptp_set_read_tx_ns(uint64_t ns); +/* Default is true (NIC bypassed). Set false to route the production + * rte_eth_timesync wrappers through the harness overrides. */ +void ut_ptp_set_no_timesync(ut_ptp_ctx* ctx, bool no_timesync); +void ut_ptp_set_coefficient(ut_ptp_ctx* ctx, double coefficient); +void ut_ptp_set_last_sync_ts(ut_ptp_ctx* ctx, uint64_t last_sync_ts); + +/* ── exercise the production code under test ──────────────────────────── */ +void ut_ptp_adjust_delta(ut_ptp_ctx* ctx, int64_t delta, bool error_correct); +/* Run the static delay_req tx-timestamp (t3) alarm handler. */ +void ut_ptp_run_t3_handler(ut_ptp_ctx* ctx); +/* Run the static single-owner completion attempt for the current cycle. */ +void ut_ptp_try_complete_result(ut_ptp_ctx* ctx); + +/* Direct wrapper over the static `pi_sample`. Operates on the context's + * own servo. Returns ppb; writes the resulting servo state ordinal to + * `*out_state` when non-NULL. */ +double ut_pi_sample(ut_ptp_ctx* ctx, double offset, double local_ts, int* out_state); + +/* Pure-function wrappers. */ +uint64_t ut_ptp_correct_ts(ut_ptp_ctx* ctx, uint64_t ts); +/* Host-order seconds/ns are packed into a network-order tmstamp and fed + * through the production `ptp_net_tmstamp_to_ns`. */ +uint64_t ut_ptp_net_tmstamp_to_ns(uint16_t sec_msb, uint32_t sec_lsb, uint32_t ns); + +/* ── getters ──────────────────────────────────────────────────────────── */ +int64_t ut_ptp_no_timesync_delta(const ut_ptp_ctx* ctx); +int64_t ut_ptp_ptp_delta(const ut_ptp_ctx* ctx); +int64_t ut_ptp_stat_delta_min(const ut_ptp_ctx* ctx); +int64_t ut_ptp_stat_delta_max(const ut_ptp_ctx* ctx); +int32_t ut_ptp_stat_delta_cnt(const ut_ptp_ctx* ctx); +uint64_t ut_ptp_delta_result_cnt(const ut_ptp_ctx* ctx); +bool ut_ptp_locked(const ut_ptp_ctx* ctx); +uint16_t ut_ptp_stat_sync_keep(const ut_ptp_ctx* ctx); +int ut_ptp_servo_count(const ut_ptp_ctx* ctx); +double ut_ptp_servo_drift(const ut_ptp_ctx* ctx); +uint64_t ut_ptp_t3(const ut_ptp_ctx* ctx); +bool ut_ptp_t3_pending(const ut_ptp_ctx* ctx); +int32_t ut_ptp_stat_t3_timeout(const ut_ptp_ctx* ctx); +int32_t ut_ptp_result_claimed(const ut_ptp_ctx* ctx); + +#ifdef __cplusplus +} +#endif + +#endif /* _UT_PTP_HARNESS_H_ */ diff --git a/tests/unit/ptp/servo_test.cpp b/tests/unit/ptp/servo_test.cpp new file mode 100644 index 000000000..3c04da92a --- /dev/null +++ b/tests/unit/ptp/servo_test.cpp @@ -0,0 +1,141 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * Pins the PI servo state machine (`pi_sample`) and the LOCKED-path + * ppb -> frequency conversion `-1 * (long)(ppb * 65.536)`. + * + * Build: meson setup build_unit -Denable_unit_tests=true && ninja -C build_unit + * Run: ./build_unit/tests/unit/UnitTest --gtest_filter='PiServo*' + */ + +#include + +#include "ptp/ptp_harness.h" + +class PiServoTest : public ::testing::Test { + protected: + void SetUp() override { + ASSERT_EQ(ut_ptp_init(), 0); + ctx_ = ut_ptp_create(); + ASSERT_NE(ctx_, nullptr); + } + void TearDown() override { + ut_ptp_destroy(ctx_); + } + ut_ptp_ctx* ctx_ = nullptr; +}; + +/* count 0/1 -> UNLOCKED, ppb == 0, count advances. */ +TEST_F(PiServoTest, FirstTwoSamplesUnlocked) { + int state = -1; + EXPECT_DOUBLE_EQ(ut_pi_sample(ctx_, 10.0, 0.0, &state), 0.0); + EXPECT_EQ(state, UT_PTP_SERVO_UNLOCKED); + EXPECT_EQ(ut_ptp_servo_count(ctx_), 1); + + EXPECT_DOUBLE_EQ(ut_pi_sample(ctx_, 20.0, 5.0, &state), 0.0); + EXPECT_EQ(state, UT_PTP_SERVO_UNLOCKED); + EXPECT_EQ(ut_ptp_servo_count(ctx_), 2); +} + +/* count 2 computes drift = (offset1 - offset0) / (local1 - local0). + * Negative slope. */ +TEST_F(PiServoTest, DriftNegativeSlope) { + int state = -1; + ut_pi_sample(ctx_, 100.0, 0.0, &state); /* offset[0]=100 local[0]=0 */ + ut_pi_sample(ctx_, 50.0, 10.0, &state); /* offset[1]=50 local[1]=10 */ + EXPECT_DOUBLE_EQ(ut_pi_sample(ctx_, 0.0, 0.0, &state), 0.0); + EXPECT_EQ(state, UT_PTP_SERVO_UNLOCKED); + EXPECT_EQ(ut_ptp_servo_count(ctx_), 3); + EXPECT_DOUBLE_EQ(ut_ptp_servo_drift(ctx_), (50.0 - 100.0) / (10.0 - 0.0)); +} + +/* count 2 with a fractional slope. */ +TEST_F(PiServoTest, DriftFractionalSlope) { + int state = -1; + ut_pi_sample(ctx_, 0.0, 0.0, &state); + ut_pi_sample(ctx_, 1.0, 3.0, &state); + ut_pi_sample(ctx_, 0.0, 0.0, &state); + EXPECT_DOUBLE_EQ(ut_ptp_servo_drift(ctx_), 1.0 / 3.0); +} + +/* count 3 -> JUMP, ppb == 0, count becomes 4. */ +TEST_F(PiServoTest, ThirdSampleJump) { + int state = -1; + ut_pi_sample(ctx_, 0.0, 0.0, &state); + ut_pi_sample(ctx_, 0.0, 1.0, &state); + ut_pi_sample(ctx_, 0.0, 2.0, &state); + EXPECT_DOUBLE_EQ(ut_pi_sample(ctx_, 0.0, 3.0, &state), 0.0); + EXPECT_EQ(state, UT_PTP_SERVO_JUMP); + EXPECT_EQ(ut_ptp_servo_count(ctx_), 4); +} + +/* count 4 -> LOCKED, ppb == 0.3*offset + (drift_before + 0.7*offset). */ +TEST_F(PiServoTest, FourthSampleLocked) { + int state = -1; + const double o0 = 40.0, l0 = 0.0; + const double o1 = 60.0, l1 = 8.0; + ut_pi_sample(ctx_, o0, l0, &state); + ut_pi_sample(ctx_, o1, l1, &state); + ut_pi_sample(ctx_, 0.0, 0.0, &state); /* sets drift */ + const double drift_before = (o1 - o0) / (l1 - l0); + ASSERT_DOUBLE_EQ(ut_ptp_servo_drift(ctx_), drift_before); + + ut_pi_sample(ctx_, 0.0, 0.0, &state); /* JUMP, count -> 4 */ + ASSERT_EQ(ut_ptp_servo_count(ctx_), 4); + + const double offset = 12.0; + const double ppb = ut_pi_sample(ctx_, offset, 0.0, &state); + EXPECT_EQ(state, UT_PTP_SERVO_LOCKED); + const double drift_after = drift_before + 0.7 * offset; + EXPECT_DOUBLE_EQ(ut_ptp_servo_drift(ctx_), drift_after); + EXPECT_DOUBLE_EQ(ppb, 0.3 * offset + drift_after); + /* count stays 4 on the LOCKED branch */ + EXPECT_EQ(ut_ptp_servo_count(ctx_), 4); +} + +/* count >= 4 stays LOCKED and accumulates the integral term every sample: + * drift(k) = drift(k-1) + 0.7*offset, ppb = 0.3*offset + drift(k). + * Pins the continuous PI tracking of servo State 4 across multiple samples + * (requirements doc section 6, "State 4 Continuous Lock PI Tracking"). */ +TEST_F(PiServoTest, LockedIntegralAccumulatesAcrossSamples) { + int state = -1; + /* reach count 4 with a flat first window so drift_before == 0 */ + ut_pi_sample(ctx_, 0.0, 0.0, &state); + ut_pi_sample(ctx_, 0.0, 1.0, &state); + ut_pi_sample(ctx_, 0.0, 0.0, &state); /* count 2 -> drift = 0/1 = 0 */ + ut_pi_sample(ctx_, 0.0, 0.0, &state); /* count 3 -> JUMP, count -> 4 */ + ASSERT_EQ(ut_ptp_servo_count(ctx_), 4); + ASSERT_DOUBLE_EQ(ut_ptp_servo_drift(ctx_), 0.0); + + /* first LOCKED sample, offset 10 */ + double ppb = ut_pi_sample(ctx_, 10.0, 0.0, &state); + EXPECT_EQ(state, UT_PTP_SERVO_LOCKED); + EXPECT_DOUBLE_EQ(ut_ptp_servo_drift(ctx_), 7.0); /* 0 + 0.7*10 */ + EXPECT_DOUBLE_EQ(ppb, 10.0); /* 0.3*10 + 7 */ + + /* second LOCKED sample, offset 20: the integrator keeps accumulating */ + ppb = ut_pi_sample(ctx_, 20.0, 0.0, &state); + EXPECT_EQ(state, UT_PTP_SERVO_LOCKED); + EXPECT_DOUBLE_EQ(ut_ptp_servo_drift(ctx_), 21.0); /* 7 + 0.7*20 */ + EXPECT_DOUBLE_EQ(ppb, 27.0); /* 0.3*20 + 21 */ + EXPECT_EQ(ut_ptp_servo_count(ctx_), 4); +} + +/* At LOCKED a zero offset leaves the integral (drift) untouched and the + * frequency output equals the held drift -- the steady-state behaviour that + * lets the loop hold a frequency once converged (the Ki integrator memory). */ +TEST_F(PiServoTest, LockedZeroOffsetHoldsFrequency) { + int state = -1; + ut_pi_sample(ctx_, 0.0, 0.0, &state); + ut_pi_sample(ctx_, 0.0, 1.0, &state); + ut_pi_sample(ctx_, 0.0, 0.0, &state); + ut_pi_sample(ctx_, 0.0, 0.0, &state); /* count -> 4 */ + ut_pi_sample(ctx_, 30.0, 0.0, &state); /* first LOCKED: drift -> 21 */ + const double held = ut_ptp_servo_drift(ctx_); + ASSERT_DOUBLE_EQ(held, 21.0); + + const double ppb = ut_pi_sample(ctx_, 0.0, 0.0, &state); + EXPECT_EQ(state, UT_PTP_SERVO_LOCKED); + EXPECT_DOUBLE_EQ(ut_ptp_servo_drift(ctx_), held); /* unchanged: + 0.7*0 */ + EXPECT_DOUBLE_EQ(ppb, held); /* 0.3*0 + drift */ +} diff --git a/tests/unit/ptp/t3_test.cpp b/tests/unit/ptp/t3_test.cpp new file mode 100644 index 000000000..7025f7bab --- /dev/null +++ b/tests/unit/ptp/t3_test.cpp @@ -0,0 +1,110 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * Pins the async delay_req tx-timestamp (t3) state machine that runs on the + * EAL alarm thread: the sequence guard that drops a stale alarm, the deadline + * give-up that clears t3_pending and counts a timeout, and the single-owner + * claim that makes a cycle's result apply exactly once when t3 and t4 complete + * concurrently on two threads. + * + * Run: ./build/tests/unit/UnitTest --gtest_filter='PtpT3*' + */ + +#include + +#include "ptp/ptp_harness.h" + +class PtpT3Test : public ::testing::Test { + protected: + void SetUp() override { + ASSERT_EQ(ut_ptp_init(), 0); + ctx_ = ut_ptp_create(); + ASSERT_NE(ctx_, nullptr); + } + void TearDown() override { + ut_ptp_set_read_tx_ret(0); /* restore default for the next test */ + ut_ptp_set_read_tx_ns(0); + ut_ptp_destroy(ctx_); + } + ut_ptp_ctx* ctx_ = nullptr; +}; + +/* A pending read whose sequence id no longer matches the latest delay_req is a + * stale alarm: the handler clears t3_pending and returns without reading a + * timestamp or counting a timeout. */ +TEST_F(PtpT3Test, SequenceGuardDropsStaleAlarm) { + ut_ptp_set_t3_sequence_id(ctx_, 7); + ut_ptp_set_t3_pending(ctx_, true, 6); /* armed for an older delay_req */ + + ut_ptp_run_t3_handler(ctx_); + + EXPECT_FALSE(ut_ptp_t3_pending(ctx_)); + EXPECT_EQ(ut_ptp_t3(ctx_), 0u); /* no timestamp accepted */ + EXPECT_EQ(ut_ptp_stat_t3_timeout(ctx_), 0); /* not a deadline timeout */ +} + +/* Once the deadline has passed and the tx timestamp is still not ready, the + * handler gives up: it clears t3_pending and increments the timeout counter so + * the next sync starts clean. */ +TEST_F(PtpT3Test, DeadlineGiveUpClearsPendingAndCounts) { + ut_ptp_set_no_timesync(ctx_, false); /* route the read into the override */ + ut_ptp_set_read_tx_ret(-1); /* tx timestamp not ready */ + ut_ptp_set_t3_sequence_id(ctx_, 5); + ut_ptp_set_t3_pending(ctx_, true, 5); + ut_ptp_set_t2(ctx_, 1000); + ut_ptp_set_t3_deadline_ns(ctx_, 1); /* already in the past */ + + ut_ptp_run_t3_handler(ctx_); + + EXPECT_FALSE(ut_ptp_t3_pending(ctx_)); + EXPECT_EQ(ut_ptp_stat_t3_timeout(ctx_), 1); + EXPECT_EQ(ut_ptp_t3(ctx_), 0u); +} + +/* A successfully read tx timestamp that predates t2 is a stale value latched by + * an earlier cycle (the single HW slot is identity-less). It must be rejected + * rather than accepted as t3, so a stale read on the deadline gives up instead + * of feeding the implausible value into the result before lock. */ +TEST_F(PtpT3Test, StaleTimestampBeforeT2Rejected) { + ut_ptp_set_no_timesync(ctx_, false); + ut_ptp_set_read_tx_ret(0); /* read "succeeds" ... */ + ut_ptp_set_read_tx_ns(500); /* ... but returns a timestamp older than t2 */ + ut_ptp_set_t2(ctx_, 1000); + ut_ptp_set_t3_sequence_id(ctx_, 9); + ut_ptp_set_t3_pending(ctx_, true, 9); + ut_ptp_set_t3_deadline_ns(ctx_, 1); /* already in the past */ + + ut_ptp_run_t3_handler(ctx_); + + EXPECT_EQ(ut_ptp_t3(ctx_), 0u); /* stale value not accepted */ + EXPECT_FALSE(ut_ptp_t3_pending(ctx_)); + EXPECT_EQ(ut_ptp_stat_t3_timeout(ctx_), 1); /* went to give-up, not success */ +} + +/* The core race fix: when all four timestamps are present, two concurrent + * completion attempts (t3 on the alarm thread, t4 on the rx thread) must apply + * the result exactly once. The second attempt -- modelling the loser thread + * that read the four stamps before the winner cleared them -- is replayed here + * by restoring the timestamps without resetting the claim; it must be a no-op. + */ +TEST_F(PtpT3Test, ResultAppliedExactlyOncePerCycle) { + ut_ptp_set_t1(ctx_, 1000); + ut_ptp_set_t2(ctx_, 2000); + ut_ptp_set_t3(ctx_, 3000); + ut_ptp_set_t4(ctx_, 4000); + + ut_ptp_try_complete_result(ctx_); /* winner consumes the cycle */ + ASSERT_EQ(ut_ptp_stat_delta_cnt(ctx_), 1); + EXPECT_EQ(ut_ptp_result_claimed(ctx_), 1); + + /* loser thread: it had observed all four stamps before the winner cleared + * them, so replay that view without a fresh cycle (no claim reset) */ + ut_ptp_set_t1(ctx_, 1000); + ut_ptp_set_t2(ctx_, 2000); + ut_ptp_set_t3(ctx_, 3000); + ut_ptp_set_t4(ctx_, 4000); + + ut_ptp_try_complete_result(ctx_); /* must NOT apply the result again */ + EXPECT_EQ(ut_ptp_stat_delta_cnt(ctx_), 1); + EXPECT_EQ(ut_ptp_result_claimed(ctx_), 1); +} From a24e9b510068160416a6cff1a468d0d7426dd26f Mon Sep 17 00:00:00 2001 From: "Kasiewicz, Marek" Date: Mon, 29 Jun 2026 08:03:50 +0000 Subject: [PATCH 03/19] Feat: Add E830 TX time launch support for TSN pacing Enable hardware launch-time (TxPP) pacing on E830 NICs used by the TSN pacing path. Register the TX timestamp dynflag/dynfield via the public mbuf dynfield API and enable RTE_ETH_TX_OFFLOAD_SEND_ON_TIMESTAMP on both port and per-queue config. Drop the PTP-locked guard in the launch-time tasklet so pacing no longer falls back to TSC, and widen timesync retry count for slower lock. Reject ST21_TX_PACING_WAY_TSN at pacing-select time on ports that don't advertise the offload, instead of silently falling through to another pacing way. Carries five ICE driver patches: TxPP timer association, PHC read-clock, 19-bit launch-time encoding, direct MMIO PHC update, and gating the send-on-timestamp offload to E830 only. Also carries the PHC-owner-init patch: on multi-port ICE devices only the PCI function reporting func_caps.ts_func_info.src_tmr_owned may start the shared PHC counter, but the PMD only does so lazily inside ice_timesync_enable() on the port the application calls it on -- if timesync is enabled only on a non-owning port, the owner's PHC never starts and rte_eth_timesync_read_time() on that port always returns 0. Bundle all six patches together with a single DPDK bump to 26.03.92_mtl so the version-gated install check in build_dpdk.sh cannot silently skip applying a subset of them. Signed-off-by: Kasiewicz, Marek --- lib/src/dev/mt_dev.c | 49 ++++-- lib/src/st2110/st_header.h | 16 +- lib/src/st2110/st_tx_ancillary_session.c | 3 +- lib/src/st2110/st_tx_audio_session.c | 2 +- lib/src/st2110/st_tx_fastmetadata_session.c | 6 +- lib/src/st2110/st_video_transmitter.c | 5 - ...ig-add-mtl-version-to-version-string.patch | 2 +- ...-timer-association-in-txtime-context.patch | 36 +++++ ...-read-clock-to-use-PHC-hardware-time.patch | 54 +++++++ ...launch-time-encoding-for-19-bit-HW-f.patch | 57 +++++++ ...e830:-use-direct-MMIO-for-PHC-update.patch | 33 ++++ .../0013-net-ice-always-init-PHC-owner.patch | 145 ++++++++++++++++++ ...te-send-on-timestamp-offload-to-e830.patch | 43 ++++++ versions.env | 2 +- 14 files changed, 419 insertions(+), 34 deletions(-) create mode 100644 patches/dpdk/26.03/0009-net-ice-fix-TxPP-timer-association-in-txtime-context.patch create mode 100644 patches/dpdk/26.03/0010-net-ice-fix-read-clock-to-use-PHC-hardware-time.patch create mode 100644 patches/dpdk/26.03/0011-net-ice-fix-TxPP-launch-time-encoding-for-19-bit-HW-f.patch create mode 100644 patches/dpdk/26.03/0012-net-ice-e830:-use-direct-MMIO-for-PHC-update.patch create mode 100644 patches/dpdk/26.03/0013-net-ice-always-init-PHC-owner.patch create mode 100644 patches/dpdk/26.03/0014-net-ice-gate-send-on-timestamp-offload-to-e830.patch diff --git a/lib/src/dev/mt_dev.c b/lib/src/dev/mt_dev.c index a516a1659..a108a9f55 100644 --- a/lib/src/dev/mt_dev.c +++ b/lib/src/dev/mt_dev.c @@ -836,7 +836,7 @@ static int dev_detect_link(struct mt_interface* inf, bool relaxed) { } static int dev_start_timesync(struct mt_interface* inf) { - int ret, i = 0, max_retry = 10; + int ret, i = 0, max_retry = 100; uint16_t port_id = inf->port_id; enum mtl_port port = inf->port; struct timespec spec; @@ -948,6 +948,12 @@ static int dev_config_port(struct mt_interface* inf) { #endif } +#if RTE_VERSION >= RTE_VERSION_NUM(23, 3, 0, 0) + if (inf->feature & MT_IF_FEATURE_TX_OFFLOAD_SEND_ON_TIMESTAMP) { + port_conf.txmode.offloads |= RTE_ETH_TX_OFFLOAD_SEND_ON_TIMESTAMP; + } +#endif + dbg("%s(%d), rss mode %d\n", __func__, port, inf->rss_mode); if (mt_has_srss(impl, port)) { struct rte_eth_rss_conf* rss_conf; @@ -1162,6 +1168,11 @@ static int dev_start_port(struct mt_interface* inf) { for (uint16_t q = 0; q < nb_tx_q; q++) { tx_port_conf = inf->dev_info.default_txconf; +#if RTE_VERSION >= RTE_VERSION_NUM(23, 3, 0, 0) + if (inf->feature & MT_IF_FEATURE_TX_OFFLOAD_SEND_ON_TIMESTAMP) { + tx_port_conf.offloads |= RTE_ETH_TX_OFFLOAD_SEND_ON_TIMESTAMP; + } +#endif ret = rte_eth_tx_queue_setup(port_id, q, nb_tx_desc, socket_id, &tx_port_conf); if (ret < 0) { err("%s(%d), rte_eth_tx_queue_setup fail %d for queue %d\n", __func__, port, ret, @@ -1432,6 +1443,11 @@ static int dev_if_init_pacing(struct mt_interface* inf) { return ret; } } + } else if (ST21_TX_PACING_WAY_TSN == inf->tx_pacing_way) { + if (!(inf->feature & MT_IF_FEATURE_TX_OFFLOAD_SEND_ON_TIMESTAMP)) { + err("%s(%d), this port not support tsn launch time\n", __func__, port); + return -EINVAL; + } } return 0; @@ -2291,22 +2307,29 @@ int mt_dev_if_init(struct mtl_main_impl* impl) { ST21_TX_PACING_WAY_TSN == inf->tx_pacing_way) { inf->feature |= MT_IF_FEATURE_TX_OFFLOAD_SEND_ON_TIMESTAMP; - int* dev_tx_timestamp_dynfield_offset_ptr = - dev_info->default_txconf.reserved_ptrs[1]; - uint64_t* dev_tx_timestamp_dynflag_ptr = dev_info->default_txconf.reserved_ptrs[0]; - ret = rte_mbuf_dyn_tx_timestamp_register(dev_tx_timestamp_dynfield_offset_ptr, - dev_tx_timestamp_dynflag_ptr); + ret = rte_mbuf_dynflag_lookup(RTE_MBUF_DYNFLAG_TX_TIMESTAMP_NAME, NULL); if (ret < 0) { - err("%s, rte_mbuf_dyn_tx_timestamp_register fail\n", __func__); - return ret; + struct rte_mbuf_dynflag dynflag_desc = {0}; + snprintf(dynflag_desc.name, sizeof(dynflag_desc.name), "%s", + RTE_MBUF_DYNFLAG_TX_TIMESTAMP_NAME); + ret = rte_mbuf_dynflag_register(&dynflag_desc); + if (ret < 0) { + err("%s, rte_mbuf_dynflag_register for tx timestamp fail\n", __func__); + return ret; + } } - - ret = rte_mbuf_dynflag_lookup(RTE_MBUF_DYNFLAG_TX_TIMESTAMP_NAME, NULL); - if (ret < 0) return ret; inf->tx_launch_time_flag = 1ULL << ret; - ret = rte_mbuf_dynfield_lookup(RTE_MBUF_DYNFIELD_TIMESTAMP_NAME, NULL); - if (ret < 0) return ret; + struct rte_mbuf_dynfield dynfield_desc = {0}; + snprintf(dynfield_desc.name, sizeof(dynfield_desc.name), "%s", + RTE_MBUF_DYNFIELD_TIMESTAMP_NAME); + dynfield_desc.size = sizeof(rte_mbuf_timestamp_t); + dynfield_desc.align = __alignof__(rte_mbuf_timestamp_t); + ret = rte_mbuf_dynfield_register(&dynfield_desc); + if (ret < 0) { + err("%s, rte_mbuf_dynfield_register for tx timestamp fail\n", __func__); + return ret; + } inf->tx_dynfield_offset = ret; } #endif diff --git a/lib/src/st2110/st_header.h b/lib/src/st2110/st_header.h index fd6b3b174..d1a7f6b27 100644 --- a/lib/src/st2110/st_header.h +++ b/lib/src/st2110/st_header.h @@ -164,11 +164,11 @@ struct st_frame_trans { /* timing for pacing */ struct st_tx_video_pacing { - long double trs; /* in ns for of 2 consecutive packets, T-Frame / N-Packets */ - long double tr_offset; /* in ns, tr offset time of each frame */ - uint32_t vrx; /* packets unit, VRX start value of each frame */ - uint32_t warm_pkts; /* packets unit, pkts for RL pacing warm boot */ - long double frame_time; /* time of the frame in nanoseconds */ + long double trs; /* in ns for of 2 consecutive packets, T-Frame / N-Packets */ + long double tr_offset; /* in ns, tr offset time of each frame */ + uint32_t vrx; /* packets unit, VRX start value of each frame */ + uint32_t warm_pkts; /* packets unit, pkts for RL pacing warm boot */ + long double frame_time; /* time of the frame in nanoseconds */ long double frame_time_sampling; /* time of the frame in sampling(90k) */ /* in ns, idle time at the end of frame, frame_time - tr_offset - (trs * pkts) */ long double frame_idle_time; @@ -696,7 +696,7 @@ struct st_rx_video_sessions_mgr { struct st_tx_audio_session_pacing { long double trs; /* in ns for of 2 consecutive packets */ long double pkt_time_sampling; /* time of each pkt in sampling */ - uint64_t cur_epochs; /* epoch of current pkt */ + uint64_t cur_epochs; /* epoch of current pkt */ /* timestamp for rtp header */ uint32_t rtp_time_stamp; long double ptp_time_cursor; @@ -1006,7 +1006,7 @@ struct st_rx_audio_sessions_mgr { struct st_tx_ancillary_session_pacing { long double frame_time; /* time of the frame in nanoseconds */ long double frame_time_sampling; /* time of the frame in sampling(90k) */ - uint64_t cur_epochs; /* epoch of current frame */ + uint64_t cur_epochs; /* epoch of current frame */ /* timestamp for rtp header */ uint32_t rtp_time_stamp; long double ptp_time_cursor; @@ -1258,7 +1258,7 @@ struct st_ancillary_transmitter_impl { struct st_tx_fastmetadata_session_pacing { long double frame_time; /* time of the frame in nanoseconds */ long double frame_time_sampling; /* time of the frame in sampling(90k) */ - uint64_t cur_epochs; /* epoch of current frame */ + uint64_t cur_epochs; /* epoch of current frame */ /* timestamp for rtp header */ uint32_t rtp_time_stamp; /* timestamp for pacing */ diff --git a/lib/src/st2110/st_tx_ancillary_session.c b/lib/src/st2110/st_tx_ancillary_session.c index e5a93e350..9457f71b6 100644 --- a/lib/src/st2110/st_tx_ancillary_session.c +++ b/lib/src/st2110/st_tx_ancillary_session.c @@ -396,8 +396,7 @@ static int tx_ancillary_session_sync_pacing(struct mtl_main_impl* impl, pacing->ptp_time_cursor = start_time_tai; pacing->tsc_time_cursor = (long double)cur_tsc + (long double)time_to_tx_ns; - dbg("%s(%d), epochs %" PRIu64 " ptp_time_cursor %Lf time_to_tx_ns %" PRId64 - "\n", + dbg("%s(%d), epochs %" PRIu64 " ptp_time_cursor %Lf time_to_tx_ns %" PRId64 "\n", __func__, s->idx, pacing->cur_epochs, pacing->ptp_time_cursor, time_to_tx_ns); return 0; diff --git a/lib/src/st2110/st_tx_audio_session.c b/lib/src/st2110/st_tx_audio_session.c index 4609a2003..81536af3f 100644 --- a/lib/src/st2110/st_tx_audio_session.c +++ b/lib/src/st2110/st_tx_audio_session.c @@ -229,7 +229,7 @@ static int tx_audio_session_init_pacing_epoch(struct mtl_main_impl* impl, } static inline uint64_t tx_audio_pacing_time(struct st_tx_audio_session_pacing* pacing, - uint64_t epochs) { + uint64_t epochs) { return nextafterl((long double)epochs * pacing->trs, INFINITY); } diff --git a/lib/src/st2110/st_tx_fastmetadata_session.c b/lib/src/st2110/st_tx_fastmetadata_session.c index 5e5f7162d..81bf08087 100644 --- a/lib/src/st2110/st_tx_fastmetadata_session.c +++ b/lib/src/st2110/st_tx_fastmetadata_session.c @@ -318,9 +318,9 @@ static int tx_fastmetadata_session_sync_pacing(struct mtl_main_impl* impl, pacing->pacing_time_stamp = tx_fastmetadata_pacing_time_stamp(pacing, epochs); pacing->rtp_time_stamp = pacing->pacing_time_stamp; pacing->tsc_time_cursor = (long double)mt_get_tsc(impl) + to_epoch; - dbg("%s(%d), epochs %" PRIu64 " time_stamp %u time_cursor %Lf to_epoch %" PRId64 "\n", __func__, - s->idx, pacing->cur_epochs, pacing->pacing_time_stamp, pacing->tsc_time_cursor, - to_epoch); + dbg("%s(%d), epochs %" PRIu64 " time_stamp %u time_cursor %Lf to_epoch %" PRId64 "\n", + __func__, s->idx, pacing->cur_epochs, pacing->pacing_time_stamp, + pacing->tsc_time_cursor, to_epoch); if (sync) { dbg("%s(%d), delay to epoch_time %Lf, cur %" PRIu64 "\n", __func__, s->idx, diff --git a/lib/src/st2110/st_video_transmitter.c b/lib/src/st2110/st_video_transmitter.c index 4f3c0781d..c8669f5fc 100644 --- a/lib/src/st2110/st_video_transmitter.c +++ b/lib/src/st2110/st_video_transmitter.c @@ -456,11 +456,6 @@ static int video_trs_launch_time_tasklet(struct mtl_main_impl* impl, enum mtl_port port = mt_port_logic2phy(s->port_maps, s_port); struct mt_interface* inf = mt_if(impl, port); - if (!mt_ptp_is_locked(impl, MTL_PORT_P)) { - /* fallback to tsc if ptp is not synced */ - return video_trs_tsc_tasklet(impl, s, s_port); - } - /* check if any inflight pkts in transmitter */ if (s->trs_inflight_num[s_port] > 0) { tx = video_trs_burst(impl, s, s_port, diff --git a/patches/dpdk/26.03/0007-config-add-mtl-version-to-version-string.patch b/patches/dpdk/26.03/0007-config-add-mtl-version-to-version-string.patch index 2f842c8c4..d3319df66 100644 --- a/patches/dpdk/26.03/0007-config-add-mtl-version-to-version-string.patch +++ b/patches/dpdk/26.03/0007-config-add-mtl-version-to-version-string.patch @@ -24,7 +24,7 @@ index 98035c1..7527942 100644 +++ b/VERSION @@ -1 +1 @@ -26.03.0 -+26.03.90_mtl_ ++26.03.91_mtl_ diff --git a/config/meson.build b/config/meson.build index 9ba7b9a..0d59d76 100644 --- a/config/meson.build diff --git a/patches/dpdk/26.03/0009-net-ice-fix-TxPP-timer-association-in-txtime-context.patch b/patches/dpdk/26.03/0009-net-ice-fix-TxPP-timer-association-in-txtime-context.patch new file mode 100644 index 000000000..d5066b1dc --- /dev/null +++ b/patches/dpdk/26.03/0009-net-ice-fix-TxPP-timer-association-in-txtime-context.patch @@ -0,0 +1,36 @@ +From 9c05e102304f23b9b6e1b8af4ec1347d514f0507 Mon Sep 17 00:00:00 2001 +From: Soumyadeep Hore +Date: Wed, 10 Jun 2026 10:35:04 +0530 +Subject: [PATCH 09/11] net/ice: fix TxPP timer association in txtime context + +The txtime context setup was missing the timer_num field assignment, +leaving it at the default value of 0. This causes incorrect PHC timer +association on multi-function NICs where the PF may be associated with +a different timer index. + +Set timer_num from hw->func_caps.ts_func_info.tmr_index_assoc to ensure +the txtime scheduling engine uses the correct PHC timer for timestamp +comparison. + +Fixes: 0b6ff09a1f19 ("net/intel: support Tx packet pacing for E830") +Cc: stable@dpdk.org +Signed-off-by: Soumyadeep Hore +--- + drivers/net/intel/ice/ice_rxtx.c | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/drivers/net/intel/ice/ice_rxtx.c b/drivers/net/intel/ice/ice_rxtx.c +index 31b74be9ba..40150748d8 100644 +--- a/drivers/net/intel/ice/ice_rxtx.c ++++ b/drivers/net/intel/ice/ice_rxtx.c +@@ -778,6 +778,7 @@ ice_setup_txtime_ctx(struct ci_tx_queue *txq, + txtime_ctx->ts_res = ICE_TXTIME_CTX_RESOLUTION_128NS; + txtime_ctx->drbell_mode_32 = ICE_TXTIME_CTX_DRBELL_MODE_32; + txtime_ctx->ts_fetch_prof_id = ICE_TXTIME_CTX_FETCH_PROF_ID_0; ++ txtime_ctx->timer_num = hw->func_caps.ts_func_info.tmr_index_assoc; + + return 0; + } +-- +2.34.1 + diff --git a/patches/dpdk/26.03/0010-net-ice-fix-read-clock-to-use-PHC-hardware-time.patch b/patches/dpdk/26.03/0010-net-ice-fix-read-clock-to-use-PHC-hardware-time.patch new file mode 100644 index 000000000..6f0726538 --- /dev/null +++ b/patches/dpdk/26.03/0010-net-ice-fix-read-clock-to-use-PHC-hardware-time.patch @@ -0,0 +1,54 @@ +From f6165f586a5628b47e5cbb68e53e9f7865ef7088 Mon Sep 17 00:00:00 2001 +From: Soumyadeep Hore +Date: Wed, 10 Jun 2026 10:35:10 +0530 +Subject: [PATCH 10/11] net/ice: fix read clock to use PHC hardware time + +The ice_read_clock() function was incorrectly using clock_gettime() +with CLOCK_MONOTONIC_RAW to return system time instead of the actual +hardware PHC time. This caused txtime scheduling to use a completely +different time base than the PHC, resulting in all scheduled timestamps +being interpreted as past time by the hardware. + +Replace the system clock read with ice_timesync_read_time() which reads +the actual PHC register value, ensuring the application can compute +correct future timestamps relative to the hardware clock. + +Fixes: 327fe144ca39 ("net/ice: support clock read") +Cc: stable@dpdk.org +Signed-off-by: Soumyadeep Hore +--- + drivers/net/intel/ice/ice_ethdev.c | 16 ++++++++-------- + 1 file changed, 8 insertions(+), 8 deletions(-) + +diff --git a/drivers/net/intel/ice/ice_ethdev.c b/drivers/net/intel/ice/ice_ethdev.c +index 0f2e7aee14..746b1aba0a 100644 +--- a/drivers/net/intel/ice/ice_ethdev.c ++++ b/drivers/net/intel/ice/ice_ethdev.c +@@ -7320,16 +7320,16 @@ ice_timesync_disable(struct rte_eth_dev *dev) + } + + static int +-ice_read_clock(__rte_unused struct rte_eth_dev *dev, uint64_t *clock) ++ice_read_clock(struct rte_eth_dev *dev, uint64_t *clock) + { +- struct timespec system_time; ++ struct timespec ts; ++ int ret; + +-#ifdef RTE_EXEC_ENV_LINUX +- clock_gettime(CLOCK_MONOTONIC_RAW, &system_time); +-#else +- clock_gettime(CLOCK_MONOTONIC, &system_time); +-#endif +- *clock = system_time.tv_sec * NSEC_PER_SEC + system_time.tv_nsec; ++ ret = ice_timesync_read_time(dev, &ts); ++ if (ret) ++ return ret; ++ ++ *clock = (uint64_t)ts.tv_sec * NSEC_PER_SEC + ts.tv_nsec; + + return 0; + } +-- +2.34.1 + diff --git a/patches/dpdk/26.03/0011-net-ice-fix-TxPP-launch-time-encoding-for-19-bit-HW-f.patch b/patches/dpdk/26.03/0011-net-ice-fix-TxPP-launch-time-encoding-for-19-bit-HW-f.patch new file mode 100644 index 000000000..d25274280 --- /dev/null +++ b/patches/dpdk/26.03/0011-net-ice-fix-TxPP-launch-time-encoding-for-19-bit-HW-f.patch @@ -0,0 +1,57 @@ +From a1b2c3d4e5f60718293a4b5c6d7e8f9011223344 Mon Sep 17 00:00:00 2001 +From: Marek Kasiewicz +Date: Mon, 22 Jun 2026 12:00:00 +0200 +Subject: [PATCH 11/13] net/ice: fix TxPP launch time encoding for 19-bit HW + field + +The E830 Tx packet pacing (TxPP) launch-time descriptor stores the launch +timestamp in the 19-bit ICE_TXTIME_STAMP_M field at 128ns resolution, so the +hardware comparator wraps naturally at 2^19 * 128ns (~67.11ms). + +ice_write_ts_desc() folded the launch time through (txtime % NS_PER_S) before +the >> 128ns shift. A full second is 7_812_500 units of 128ns, which needs 23 +bits and is not a multiple of the field's 2^19 (524288) range. The modulo +therefore makes the encoded value jump non-monotonically at every 1-second PHC +boundary and disagree with the hardware's natural-wrap comparator. The launch +engine then reads queued descriptors as past-dated, stalls draining the TXTIME +ring, and the ring-wrap duplicate-descriptor write amplifies the stall into a +periodic pacing "wedge": the queue intermittently flushes at wire rate instead +of pacing, producing bursty ~72ns inter-packet gaps and rate drift. + +Encode the raw 128ns time and let FIELD_PREP keep the low 19 bits, matching how +the hardware derives its own current time. This makes hardware pacing +monostable. + +Validated on E830 (single ST2110-20 video session, 1024-entry Tx ring): with +the modulo encoding 6 of 13 runs wedged into the bursty basin; with this fix +32 of 32 runs paced cleanly with near-zero drift, across burst sizes 1/4/16. +A/B controlled on the same NIC in the same session. + +Fixes: 0b6ff09a1f19 ("net/intel: support Tx packet pacing for E830") +Cc: stable@dpdk.org +Signed-off-by: Marek Kasiewicz +--- + drivers/net/intel/ice/ice_rxtx.c | 8 +++++++- + 1 file changed, 7 insertions(+), 1 deletion(-) + +diff --git a/drivers/net/intel/ice/ice_rxtx.c b/drivers/net/intel/ice/ice_rxtx.c +index 40150748d8..cf8d38fff0 100644 +--- a/drivers/net/intel/ice/ice_rxtx.c ++++ b/drivers/net/intel/ice/ice_rxtx.c +@@ -3082,7 +3082,13 @@ ice_write_ts_desc(struct ci_tx_queue *txq, + uint16_t ts_id) + { + uint64_t txtime = *RTE_MBUF_DYNFIELD(tx_pkt, txq->tsq->ts_offset, uint64_t *); +- uint32_t tstamp = (uint32_t)(txtime % NS_PER_S) >> ICE_TXTIME_CTX_RESOLUTION_128NS; ++ /* The 19-bit ICE_TXTIME_STAMP_M field wraps naturally at 2^19 * 128ns ++ * (~67.11ms). Folding through (% NS_PER_S) first does not fit that field ++ * (a second needs 23 bits) and only injects a misaligned discontinuity at ++ * every 1-second PHC boundary. Encode the raw 128ns time and let FIELD_PREP ++ * keep the low 19 bits, matching the hardware's natural-wrap comparator. ++ */ ++ uint32_t tstamp = (uint32_t)(txtime >> ICE_TXTIME_CTX_RESOLUTION_128NS); + const uint32_t desc_tx_id = (tx_id == 0) ? txq->nb_tx_desc : tx_id; + __le32 ts_desc = rte_cpu_to_le_32(FIELD_PREP(ICE_TXTIME_TX_DESC_IDX_M, desc_tx_id) | + FIELD_PREP(ICE_TXTIME_STAMP_M, tstamp)); +-- +2.34.1 diff --git a/patches/dpdk/26.03/0012-net-ice-e830:-use-direct-MMIO-for-PHC-update.patch b/patches/dpdk/26.03/0012-net-ice-e830:-use-direct-MMIO-for-PHC-update.patch new file mode 100644 index 000000000..e87359dec --- /dev/null +++ b/patches/dpdk/26.03/0012-net-ice-e830:-use-direct-MMIO-for-PHC-update.patch @@ -0,0 +1,33 @@ +From 0000000000000000000000000000000000000002 Mon Sep 17 00:00:00 2001 +From: "Kasiewicz, Marek" +Date: Thu, 8 May 2025 06:30:00 +0000 +Subject: [PATCH 12/12] ice: e830: use direct MMIO for PHY timer command + +E830 has an internal PHY, so ice_ptp_port_cmd_e830() must not use +the SBQ (sideband queue) path designed for E810's external PHY. +Replace ice_write_phy_reg_e810_lp() with a direct wr32() to avoid +ICE_ERR_AQ_ERROR (-100) on PTP clock adjustments. + +Signed-off-by: Kasiewicz, Marek +--- + drivers/net/intel/ice/base/ice_ptp_hw.c | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +diff --git a/drivers/net/intel/ice/base/ice_ptp_hw.c b/drivers/net/intel/ice/base/ice_ptp_hw.c +--- a/drivers/net/intel/ice/base/ice_ptp_hw.c ++++ b/drivers/net/intel/ice/base/ice_ptp_hw.c +@@ -5627,8 +5627,10 @@ + { + u32 val = ice_ptp_tmr_cmd_to_port_reg(hw, cmd); + +- return ice_write_phy_reg_e810_lp(hw, E830_ETH_GLTSYN_CMD, val, +- lock_sbq); ++ (void)lock_sbq; ++ wr32(hw, E830_ETH_GLTSYN_CMD, val); ++ ++ return 0; + } + + /** +-- +2.43.0 \ No newline at end of file diff --git a/patches/dpdk/26.03/0013-net-ice-always-init-PHC-owner.patch b/patches/dpdk/26.03/0013-net-ice-always-init-PHC-owner.patch new file mode 100644 index 000000000..b34c4f812 --- /dev/null +++ b/patches/dpdk/26.03/0013-net-ice-always-init-PHC-owner.patch @@ -0,0 +1,145 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MTL Contributor +Date: Fri, 3 Jul 2026 00:00:00 +0000 +Subject: [PATCH] net/ice: init PHC owner when enabling timesync on a + non-owning port + +On multi-port ICE devices (E810/E830), only the PCI function reporting +func_caps.ts_func_info.src_tmr_owned is allowed to program the shared +GLTSYN_ENA/GLTSYN_INCVAL registers that start the PHC counter. The +kernel ice driver initializes that owner unconditionally for every +function at probe time (ice_ptp_init()), regardless of which port +userspace later interacts with. + +This PMD instead only starts the owner's PHC lazily, inside +ice_timesync_enable(), gated on hw->func_caps.ts_func_info.src_tmr_owned +for whichever port the application calls it on. If an application +enables IEEE1588 timesync only on a non-owning port (e.g. the second +port of a dual-port card) without also enabling it on the owning port, +ice_ptp_init_phc()/ice_ptp_write_incval() are never called for anyone, +the shared GLTSYN counter never starts incrementing, and +rte_eth_timesync_read_time() on that port always returns 0. + +Look up the sibling ethdev on the same physical NIC (same PCI +domain/bus/device, differing function) that owns the timer and make +sure it has been started, mirroring what the kernel driver does +unconditionally at probe time. If the owner port has not been probed +in this process, log a clear warning explaining why reads will be 0, +rather than silently returning bogus timestamps. + +Signed-off-by: MTL Contributor +--- + drivers/net/intel/ice/ice_ethdev.c | 91 +++++++++++++++++++++++++++++++++++++ + 1 file changed, 91 insertions(+) + +diff --git a/drivers/net/intel/ice/ice_ethdev.c b/drivers/net/intel/ice/ice_ethdev.c +index 746b1aba..cc1ee7be 100644 +--- a/drivers/net/intel/ice/ice_ethdev.c ++++ b/drivers/net/intel/ice/ice_ethdev.c +@@ -7051,6 +7051,53 @@ static int ice_ptp_write_init(struct ice_hw *hw) + return ice_ptp_init_time(hw, ns, true); + } + ++/** ++ * ice_timesync_find_src_tmr_owner - find the sibling port owning the PHC ++ * @dev: port requesting the lookup ++ * ++ * On multi-port ICE devices only one PCI function (the one with ++ * func_caps.ts_func_info.src_tmr_owned set) is allowed to program the ++ * GLTSYN_ENA/GLTSYN_INCVAL registers that start the shared PHC counter. ++ * The kernel ice driver initializes that owner unconditionally for every ++ * function at probe time (see ice_ptp_init()), regardless of which port ++ * userspace later touches. This PMD only does so lazily, inside ++ * ice_timesync_enable(), for whichever port the application calls it on. ++ * ++ * Look up the sibling ethdev on the same physical NIC (same PCI ++ * domain/bus/device, different function) that owns the timer, so the ++ * caller can make sure it has been started too. ++ * ++ * Return: the owner's rte_eth_dev, or NULL if it is not probed in this ++ * process. ++ */ ++static struct rte_eth_dev * ++ice_timesync_find_src_tmr_owner(struct rte_eth_dev *dev) ++{ ++ struct rte_pci_device *pci_dev = ICE_DEV_TO_PCI(dev); ++ uint16_t port_id; ++ ++ RTE_ETH_FOREACH_DEV(port_id) { ++ struct rte_eth_dev *sibling = &rte_eth_devices[port_id]; ++ struct rte_pci_device *sibling_pci; ++ struct ice_hw *sibling_hw; ++ ++ if (sibling == dev || !is_ice_supported(sibling)) ++ continue; ++ ++ sibling_pci = ICE_DEV_TO_PCI(sibling); ++ if (sibling_pci->addr.domain != pci_dev->addr.domain || ++ sibling_pci->addr.bus != pci_dev->addr.bus || ++ sibling_pci->addr.devid != pci_dev->addr.devid) ++ continue; ++ ++ sibling_hw = ICE_DEV_PRIVATE_TO_HW(sibling->data->dev_private); ++ if (sibling_hw->func_caps.ts_func_info.src_tmr_owned) ++ return sibling; ++ } ++ ++ return NULL; ++} ++ + static int + ice_timesync_enable(struct rte_eth_dev *dev) + { +@@ -7081,6 +7128,50 @@ ice_timesync_enable(struct rte_eth_dev *dev) + "Failed to write PHC increment time value"); + return -1; + } ++ } else { ++ /* ++ * This function does not own the shared source timer. Make ++ * sure the owner has actually been started: unlike the ++ * kernel driver, this PMD only starts it lazily on whichever ++ * port ice_timesync_enable() is called for, so enabling ++ * timesync only on a non-owning port would otherwise leave ++ * the shared GLTSYN counter never incrementing, and every ++ * PHC read on this port would return 0. ++ */ ++ struct rte_eth_dev *owner_dev = ++ ice_timesync_find_src_tmr_owner(dev); ++ ++ if (!owner_dev) { ++ PMD_DRV_LOG(WARNING, ++ "PHC source timer owner port for %s is not probed in this process; " ++ "PHC reads on this port will return 0 unless the owner port is " ++ "also opened and timesync-enabled", ++ dev->data->name); ++ } else { ++ struct ice_hw *owner_hw = ++ ICE_DEV_PRIVATE_TO_HW(owner_dev->data->dev_private); ++ struct ice_adapter *owner_ad = ++ ICE_DEV_PRIVATE_TO_ADAPTER(owner_dev->data->dev_private); ++ ++ if (!owner_ad->ptp_ena) { ++ ret = ice_ptp_init_phc(owner_hw); ++ if (ret) { ++ PMD_DRV_LOG(ERR, ++ "Failed to initialize PHC on owner port %s", ++ owner_dev->data->name); ++ return -1; ++ } ++ ++ ret = ice_ptp_write_incval(owner_hw, ++ ICE_PTP_NOMINAL_INCVAL_E810, true); ++ if (ret) { ++ PMD_DRV_LOG(ERR, ++ "Failed to write PHC increment time value on owner port %s", ++ owner_dev->data->name); ++ return -1; ++ } ++ } ++ } + } + + if (!ice_ptp_lock(hw)) { +-- +2.34.1 diff --git a/patches/dpdk/26.03/0014-net-ice-gate-send-on-timestamp-offload-to-e830.patch b/patches/dpdk/26.03/0014-net-ice-gate-send-on-timestamp-offload-to-e830.patch new file mode 100644 index 000000000..c71d2f105 --- /dev/null +++ b/patches/dpdk/26.03/0014-net-ice-gate-send-on-timestamp-offload-to-e830.patch @@ -0,0 +1,43 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MTL Contributor +Date: Tue, 7 Jul 2026 00:00:00 +0000 +Subject: [PATCH] net/ice: gate SEND_ON_TIMESTAMP offload capability to E830 + +Only E830 implements the TxPP (Tx packet pacing) launch-time hardware +that RTE_ETH_TX_OFFLOAD_SEND_ON_TIMESTAMP depends on; E810 has no such +launch-time descriptor engine. ice_dev_info_get() nonetheless +advertised the offload unconditionally for every ICE part. + +Applications that pick a TX pacing mode based on advertised +capabilities (e.g. preferring launch-time/TSN pacing whenever +RTE_ETH_TX_OFFLOAD_SEND_ON_TIMESTAMP is present) would then select it +on E810 ports too, where the hardware has no launch-time engine to +honor the requested timestamp, silently sending packets unpaced +instead. + +Only set the capability bit when hw->phy_model == ICE_PHY_E830. + +Signed-off-by: MTL Contributor +--- + drivers/net/intel/ice/ice_ethdev.c | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +diff --git a/drivers/net/intel/ice/ice_ethdev.c b/drivers/net/intel/ice/ice_ethdev.c +index 0f2e7aee14..030d53aded 100644 +--- a/drivers/net/intel/ice/ice_ethdev.c ++++ b/drivers/net/intel/ice/ice_ethdev.c +@@ -4568,8 +4568,10 @@ ice_dev_info_get(struct rte_eth_dev *dev, struct rte_eth_dev_info *dev_info) + RTE_ETH_TX_OFFLOAD_VXLAN_TNL_TSO | + RTE_ETH_TX_OFFLOAD_GRE_TNL_TSO | + RTE_ETH_TX_OFFLOAD_IPIP_TNL_TSO | +- RTE_ETH_TX_OFFLOAD_GENEVE_TNL_TSO | +- RTE_ETH_TX_OFFLOAD_SEND_ON_TIMESTAMP; ++ RTE_ETH_TX_OFFLOAD_GENEVE_TNL_TSO; ++ if (hw->phy_model == ICE_PHY_E830) ++ dev_info->tx_offload_capa |= ++ RTE_ETH_TX_OFFLOAD_SEND_ON_TIMESTAMP; + dev_info->flow_type_rss_offloads |= ICE_RSS_OFFLOAD_ALL; + } + +-- +2.43.0 diff --git a/versions.env b/versions.env index 862957b52..1bc27c900 100644 --- a/versions.env +++ b/versions.env @@ -1,5 +1,5 @@ DPDK_VER=26.03 -DPDK_MTL_MINOR_VER=90 +DPDK_MTL_MINOR_VER=91 ICE_VER=2.6.6 ICE_DMID=921605 ONE_API_GPU_VER=1.22.3 From 05ae05272c40ad70770e604cf777818e0a60365c Mon Sep 17 00:00:00 2001 From: "Kasiewicz, Marek" Date: Tue, 7 Jul 2026 08:52:33 +0000 Subject: [PATCH 04/19] Fix: Prevent stuck epoch recovery on ST20/ST40 TX pacing A PTP step forward that lands the clock beyond max_onward_epochs was handled inconsistently between the onward and late branches of calc_frame_count_since_epoch()/tx_ancillary_calc_epoch(): - The onward branch resynced to current_epoch only when required_tai was already zero, so a caller-supplied user timestamp kept the stale next_free_epoch/next_free_frame_slot forever, never catching up to the stepped clock. - The late branch unconditionally overwrote a valid required_tai with frame_count_tai, discarding the caller's timestamp. - A required_tai that rounds down to epoch/frame_count 0 collided with the sentinel used elsewhere to mean "no user timestamp", silently falling back to whatever stale epoch was already in flight. - EXACT_USER_PACING with a zero timestamp went unflagged in stat_error_user_timestamp for callers only ever driving the frame-level tv_pacing_required_tai()/tx_ancillary_pacing_required_tai() path (RTP-level pacing calls calc_frame_count_since_epoch() directly with a non-zero required_tai per packet, so flagging it there would have produced false positives every packet). Guard the onward-range resync with the onward-gap check regardless of required_tai, preserve required_tai through both branches unless it is absent, fall back to real time when required_tai rounds to zero, and move the zero-timestamp stat increment to the frame-level helpers. Signed-off-by: Kasiewicz, Marek --- lib/src/st2110/st_tx_ancillary_session.c | 18 +++++++++++++----- lib/src/st2110/st_tx_video_session.c | 21 ++++++++++++--------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/lib/src/st2110/st_tx_ancillary_session.c b/lib/src/st2110/st_tx_ancillary_session.c index 9457f71b6..d2828ad28 100644 --- a/lib/src/st2110/st_tx_ancillary_session.c +++ b/lib/src/st2110/st_tx_ancillary_session.c @@ -302,6 +302,7 @@ static uint64_t tx_ancillary_pacing_required_tai(struct st_tx_ancillary_session_ if (!(s->ops.flags & ST40_TX_FLAG_USER_PACING)) return 0; if (!timestamp) { if (s->ops.flags & ST40_TX_FLAG_EXACT_USER_PACING) { + s->port_user_stats.common.stat_error_user_timestamp++; err("%s(%d), EXACT_USER_PACING requires non-zero timestamp\n", __func__, s->idx); } return 0; @@ -345,17 +346,24 @@ static inline uint64_t tx_ancillary_calc_epoch(struct st_tx_ancillary_session_im if (required_tai) { epoch = (required_tai + pacing->frame_time / 2) / pacing->frame_time; tx_ancillary_validate_user_timestamp(s, epoch, current_epoch); + /* epoch 0 collides with the "unset" sentinel used elsewhere; fall back to + * real time rather than honor a rounded-to-zero required_tai */ + if (!epoch) epoch = current_epoch; } if (current_epoch <= next_free_epoch) { - if (next_free_epoch - current_epoch > pacing->max_onward_epochs) { + uint64_t onward = next_free_epoch - current_epoch; + if (onward > pacing->max_onward_epochs) { + /* current time is out of onward range, resync to real time instead of + * advancing by one epoch forever */ dbg("%s(%d), onward range exceeded, next_free_epoch %" PRIu64 ", current_epoch %" PRIu64 "\n", __func__, s->idx, next_free_epoch, current_epoch); - s->port_user_stats.common.stat_epoch_onward += (next_free_epoch - current_epoch); + s->port_user_stats.common.stat_epoch_onward += onward; + if (!required_tai) epoch = current_epoch; + } else if (!required_tai) { + epoch = next_free_epoch; } - - if (!required_tai) epoch = next_free_epoch; } else { dbg("%s(%d), frame is late, current_epoch %" PRIu64 " next_free_epoch %" PRIu64 "\n", __func__, s->idx, current_epoch, next_free_epoch); @@ -365,7 +373,7 @@ static inline uint64_t tx_ancillary_calc_epoch(struct st_tx_ancillary_session_im s->ops.notify_frame_late(s->ops.priv, current_epoch - next_free_epoch); } - epoch = current_epoch; + if (!required_tai) epoch = current_epoch; } return epoch; diff --git a/lib/src/st2110/st_tx_video_session.c b/lib/src/st2110/st_tx_video_session.c index 7fd006b57..d245b713e 100644 --- a/lib/src/st2110/st_tx_video_session.c +++ b/lib/src/st2110/st_tx_video_session.c @@ -655,21 +655,23 @@ static inline uint64_t calc_frame_count_since_epoch(struct st_tx_video_session_i if (required_tai) { frame_count = (required_tai + s->pacing.frame_time / 2) / s->pacing.frame_time; validate_user_timestamp(s, frame_count, frame_count_tai); + /* frame_count 0 collides with the "unset" sentinel used elsewhere; fall back to + * real time rather than honor a rounded-to-zero required_tai */ + if (!frame_count) frame_count = frame_count_tai; } if (frame_count_tai <= next_free_frame_slot) { /* There is time buffer until the next available frame time window */ - if (next_free_frame_slot - frame_count_tai > s->pacing.max_onward_epochs) { - /* current time is out of onward range, just note this and still move to next free - * slot */ + uint64_t onward = next_free_frame_slot - frame_count_tai; + if (onward > s->pacing.max_onward_epochs) { + /* current time is out of onward range, resync to real time instead of + * advancing by one epoch forever */ dbg("%s(%d), onward range exceeded, next_free_frame_slot %" PRIu64 ", frame_count_tai %" PRIu64 "\n", __func__, s->idx, next_free_frame_slot, frame_count_tai); - s->port_user_stats.common.stat_epoch_onward += - (next_free_frame_slot - frame_count_tai); - } - - if (!required_tai) { + s->port_user_stats.common.stat_epoch_onward += onward; + if (!required_tai) frame_count = frame_count_tai; + } else if (!required_tai) { frame_count = next_free_frame_slot; } @@ -683,7 +685,7 @@ static inline uint64_t calc_frame_count_since_epoch(struct st_tx_video_session_i s->ops.notify_frame_late(s->ops.priv, frame_count_tai - next_free_frame_slot); } - frame_count = frame_count_tai; + if (!required_tai) frame_count = frame_count_tai; } return frame_count; @@ -1733,6 +1735,7 @@ static uint64_t tv_pacing_required_tai(struct st_tx_video_session_impl* s, if (!(s->ops.flags & ST20_TX_FLAG_USER_PACING)) return 0; if (!timestamp) { if (s->ops.flags & ST20_TX_FLAG_EXACT_USER_PACING) { + s->port_user_stats.common.stat_error_user_timestamp++; err("%s(%d), EXACT_USER_PACING requires non-zero timestamp\n", __func__, s->idx); } return 0; From b51f5aec523fa5fb4c31243292897caa317b0256 Mon Sep 17 00:00:00 2001 From: "Kasiewicz, Marek" Date: Tue, 7 Jul 2026 08:53:00 +0000 Subject: [PATCH 05/19] Test: Add unit coverage for ST20 TX epoch/pacing math Adds a white-box harness that includes st_tx_video_session.c directly so calc_frame_count_since_epoch(), tv_sync_pacing(), and tv_pacing_required_tai() can be exercised without DPDK EAL, mbuf pools, or packet I/O. mt_get_tsc() is mocked with a preprocessor seam local to the harness: mt_main.h is included first under its real name so the genuine definition compiles normally, then mt_get_tsc is redefined to the mock before including the production .c file, redirecting only the call sites written inside it. mt_get_ptp_time() reuses the existing production ptp_get_time_fn extensibility point instead. 14 epoch-recovery tests and 5 pacing tests cover the onward/late resync paths, the epoch-0 sentinel collision, required_tai preservation across resync, and the EXACT_USER_PACING zero-timestamp stat. Signed-off-by: Kasiewicz, Marek --- tests/unit/README.md | 8 +- tests/unit/meson.build | 3 + tests/unit/session/st20_tx/epoch_test.cpp | 176 ++++++++++++++++++ tests/unit/session/st20_tx/pacing_test.cpp | 105 +++++++++++ tests/unit/session/st20_tx_harness.c | 205 +++++++++++++++++++++ tests/unit/session/st20_tx_harness.h | 78 ++++++++ 6 files changed, 572 insertions(+), 3 deletions(-) create mode 100644 tests/unit/session/st20_tx/epoch_test.cpp create mode 100644 tests/unit/session/st20_tx/pacing_test.cpp create mode 100644 tests/unit/session/st20_tx_harness.c create mode 100644 tests/unit/session/st20_tx_harness.h diff --git a/tests/unit/README.md b/tests/unit/README.md index 457ac291f..81cae7149 100644 --- a/tests/unit/README.md +++ b/tests/unit/README.md @@ -15,9 +15,11 @@ asserts on production behaviour. The suite covers ST 2110-20 (video), ST 2110-30 (audio), ST 2110-40 (ancillary), and the ST 2110-40 pipeline. Tests run on any developer laptop in a fraction of a second. -TX paths, DMA, kernel-socket / AF_XDP backends, and multi-process scenarios -are out of scope — see [`tests/integration_tests/`](../integration_tests/) and -[`tests/validation/`](../validation/) for those. +TX packet building/queueing, DMA, kernel-socket / AF_XDP backends, and +multi-process scenarios are out of scope — see +[`tests/integration_tests/`](../integration_tests/) and +[`tests/validation/`](../validation/) for those. TX epoch/pacing *math* +(`session/st20_tx_harness.c`) is covered here. ## What makes this different diff --git a/tests/unit/meson.build b/tests/unit/meson.build index ffe477dd1..bf45276d6 100644 --- a/tests/unit/meson.build +++ b/tests/unit/meson.build @@ -58,6 +58,9 @@ unit_sources = [ 'session/st20/stats_test.cpp', 'session/st20/err_packets_test.cpp', 'session/st20/timestamp_source_test.cpp', + 'session/st20_tx_harness.c', + 'session/st20_tx/epoch_test.cpp', + 'session/st20_tx/pacing_test.cpp', 'pipeline/st20p_harness.c', 'pipeline/st20p_test.cpp', 'pipeline/st20p_rx_concurrency_test.cpp', diff --git a/tests/unit/session/st20_tx/epoch_test.cpp b/tests/unit/session/st20_tx/epoch_test.cpp new file mode 100644 index 000000000..647b6d23f --- /dev/null +++ b/tests/unit/session/st20_tx/epoch_test.cpp @@ -0,0 +1,176 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * Pins the epoch/onward-resync math in + * `calc_frame_count_since_epoch()` (st_tx_video_session.c): steady-state + * advance, the max_onward_epochs boundary, the PTP-step resync fix, the + * late-frame drop path, and required_tai validation. + * + * Build: meson setup build_unit -Denable_unit_tests=true && ninja -C build_unit + * Run: ./build_unit/tests/unit/UnitTest --gtest_filter='St20TxEpochTest.*' + */ + +#include + +#include + +#include "session/st20_tx_harness.h" + +class St20TxEpochTest : public ::testing::Test { + protected: + void SetUp() override { + ASSERT_EQ(ut_txv_init(), 0); + ctx_ = ut_txv_create(); + ASSERT_NE(ctx_, nullptr); + ut_txv_set_frame_time(ctx_, 1000000.0L); + ut_txv_set_max_onward_epochs(ctx_, 3); + } + void TearDown() override { + ut_txv_destroy(ctx_); + } + ut_txv_ctx* ctx_ = nullptr; +}; + +TEST_F(St20TxEpochTest, SteadyStateAdvancesByOneEpoch) { + ut_txv_set_cur_epochs(ctx_, 9); /* next_free_frame_slot = 10 */ + uint64_t frame_count = ut_txv_calc_frame_count_since_epoch(ctx_, 10000000, 0); + EXPECT_EQ(frame_count, 10u); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); +} + +TEST_F(St20TxEpochTest, OnwardGapAtBoundaryDoesNotResync) { + ut_txv_set_cur_epochs(ctx_, 9); /* next=10 */ + uint64_t frame_count = + ut_txv_calc_frame_count_since_epoch(ctx_, 7000000, 0); /* tai=7, onward=3==max */ + EXPECT_EQ(frame_count, 10u); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); +} + +TEST_F(St20TxEpochTest, OnwardGapBeyondBoundaryResyncs) { + ut_txv_set_cur_epochs(ctx_, 9); /* next=10 */ + uint64_t frame_count = + ut_txv_calc_frame_count_since_epoch(ctx_, 6000000, 0); /* tai=6, onward=4>max */ + EXPECT_EQ(frame_count, 6u); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 4u); +} + +TEST_F(St20TxEpochTest, LargePtpStepResyncsInOneCall) { + ut_txv_set_cur_epochs(ctx_, 1000000); /* simulates a stale epoch after a PTP step */ + uint64_t frame_count = + ut_txv_calc_frame_count_since_epoch(ctx_, 5000000, 0); /* tai=5 */ + EXPECT_EQ(frame_count, 5u); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 999996u); /* next(1000001) - tai(5) */ + + /* caller commits the resync (as tv_sync_pacing does) and time advances by + * one frame -- confirm the gap stays closed, no further resync needed */ + ut_txv_set_cur_epochs(ctx_, frame_count); + uint64_t frame_count2 = ut_txv_calc_frame_count_since_epoch(ctx_, 6000000, 0); + EXPECT_EQ(frame_count2, 6u); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 999996u); /* unchanged */ +} + +TEST_F(St20TxEpochTest, LatePathBumpsDropAndNotifiesFrameLate) { + ut_txv_set_cur_epochs(ctx_, 2); /* next_free_frame_slot=3 */ + uint64_t frame_count = + ut_txv_calc_frame_count_since_epoch(ctx_, 10000000, 0); /* tai=10 */ + EXPECT_EQ(frame_count, 10u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 7u); + EXPECT_EQ(ut_txv_notify_late_calls(ctx_), 1); + EXPECT_EQ(ut_txv_notify_late_last_delta(ctx_), 7u); +} + +TEST_F(St20TxEpochTest, RequiredTaiInThePastFlagsStat) { + ut_txv_set_cur_epochs(ctx_, 9); /* next=10, onward=0 (normal path) */ + uint64_t frame_count = ut_txv_calc_frame_count_since_epoch(ctx_, 10000000, 5000000); + EXPECT_EQ(frame_count, 5u); /* required_tai-derived value, not overridden */ + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u); +} + +TEST_F(St20TxEpochTest, RequiredTaiTooFarFutureFlagsStat) { + ut_txv_set_cur_epochs(ctx_, 9); /* current frame count = 10, limit = +1000 */ + uint64_t required_tai = 1011ull * 1000000; + uint64_t frame_count = + ut_txv_calc_frame_count_since_epoch(ctx_, 10000000, required_tai); + EXPECT_EQ(frame_count, 1011u); + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u); +} + +TEST_F(St20TxEpochTest, RequiredTaiPreservedThroughOnwardResync) { + ut_txv_set_cur_epochs(ctx_, 1000000); /* forces the onward-resync branch */ + uint64_t frame_count = ut_txv_calc_frame_count_since_epoch(ctx_, 5000000, 2000000); + EXPECT_EQ(frame_count, 2u); /* required_tai-derived value survives the resync */ + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 999996u); +} + +TEST_F(St20TxEpochTest, RequiredTaiExactlyAtInThePastBoundaryDoesNotFlag) { + ut_txv_set_cur_epochs(ctx_, 9); /* current frame count = 10 */ + ut_txv_calc_frame_count_since_epoch(ctx_, 10000000, + 10000000); /* required frame count == 10 */ + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 0u); +} + +TEST_F(St20TxEpochTest, RequiredTaiExactlyAtFutureBoundaryDoesNotFlag) { + ut_txv_set_cur_epochs(ctx_, 9); /* current frame count = 10, limit = +1000 */ + ut_txv_calc_frame_count_since_epoch(ctx_, 10000000, + 1010000000ull); /* required frame count == 1010 */ + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 0u); +} + +TEST_F(St20TxEpochTest, CurEpochsWraparoundIsWellDefinedNotUB) { + ut_txv_set_cur_epochs(ctx_, UINT64_MAX); /* next_free_frame_slot wraps to 0 */ + uint64_t frame_count = ut_txv_calc_frame_count_since_epoch(ctx_, 5000000, 0); + EXPECT_EQ(frame_count, 5u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 5u); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); +} + +TEST_F(St20TxEpochTest, CurTaiNearUint64MaxNoCrash) { + ut_txv_set_cur_epochs(ctx_, 0); /* next_free_frame_slot=1 */ + uint64_t cur_tai = UINT64_MAX; + uint64_t expected_frame_count_tai = cur_tai / 1000000ull; + uint64_t frame_count = ut_txv_calc_frame_count_since_epoch(ctx_, cur_tai, 0); + EXPECT_EQ(frame_count, expected_frame_count_tai); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), expected_frame_count_tai - 1); +} + +/* Reproduces the required_tai asymmetry flagged in review: unlike the onward + * branch (which now preserves a validated required_tai through resync, see + * RequiredTaiPreservedThroughOnwardResync above), the late (else) branch has + * no `if (!required_tai)` guard -- it unconditionally overwrites frame_count + * with frame_count_tai, silently discarding a validated required_tai. This + * asserts the CORRECT/expected behavior (symmetric with the onward branch) + * and currently FAILS against production code, confirming the defect is + * real rather than hypothetical. */ +TEST_F(St20TxEpochTest, LatePathShouldPreserveRequiredTaiLikeOnwardBranch) { + ut_txv_set_cur_epochs(ctx_, 2); /* next_free_frame_slot=3 */ + uint64_t frame_count = ut_txv_calc_frame_count_since_epoch(ctx_, 10000000, 4000000); + EXPECT_EQ(frame_count, 4u) << "late branch should preserve the required_tai-derived " + "frame_count (4), not overwrite it with frame_count_tai " + "(10)"; + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 7u); +} + +/* Reproduces a second, independent gap: validate_user_timestamp() flags an + * out-of-range required_tai (e.g. a caller unit-conversion bug -- passing + * milliseconds where nanoseconds are expected) via a stat counter, but the + * wildly-wrong derived frame_count is still returned and used verbatim -- + * there is no fallback to real time once validation fails. Contrast with + * LargePtpStepResyncsInOneCall, where a bad cur_epochs DOES self-heal via + * the onward-resync branch: here, an out-of-range *required_tai* has no + * equivalent self-healing path in the normal (non-onward, non-late) branch. + * This asserts the safer/expected fallback behavior and currently FAILS. */ +TEST_F(St20TxEpochTest, InvalidRequiredTaiIsFlaggedButNotClampedToRealTime) { + ut_txv_set_cur_epochs(ctx_, 9); /* next_free_frame_slot=10, onward=0 (normal path) */ + uint64_t required_tai = 10; /* caller bug: ms instead of ns, ~1e6x too small */ + uint64_t frame_count = + ut_txv_calc_frame_count_since_epoch(ctx_, 10000000, required_tai); + + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u) + << "validation should flag the out-of-range required_tai"; + EXPECT_EQ(frame_count, 10u) << "an out-of-range required_tai should fall back to " + "real time (frame 10), not be honored verbatim (frame " + << frame_count << ")"; +} diff --git a/tests/unit/session/st20_tx/pacing_test.cpp b/tests/unit/session/st20_tx/pacing_test.cpp new file mode 100644 index 000000000..2d4768d54 --- /dev/null +++ b/tests/unit/session/st20_tx/pacing_test.cpp @@ -0,0 +1,105 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * Pins the cursor-derivation math in `tv_sync_pacing()` (st_tx_video_session.c): + * normal tsc/ptp cursor derivation, the ST20_TX_FLAG_EXACT_USER_PACING + * bypass, and the negative time_to_tx_ns clamp. + * + * Build: meson setup build_unit -Denable_unit_tests=true && ninja -C build_unit + * Run: ./build_unit/tests/unit/UnitTest --gtest_filter='St20TxSyncPacingTest.*' + */ + +#include + +#include "session/st20_tx_harness.h" + +class St20TxSyncPacingTest : public ::testing::Test { + protected: + void SetUp() override { + ASSERT_EQ(ut_txv_init(), 0); + ctx_ = ut_txv_create(); + ASSERT_NE(ctx_, nullptr); + ut_txv_set_frame_time(ctx_, 1000000.0L); + ut_txv_set_max_onward_epochs(ctx_, 3); + } + void TearDown() override { + ut_txv_destroy(ctx_); + } + ut_txv_ctx* ctx_ = nullptr; +}; + +TEST_F(St20TxSyncPacingTest, NormalCursorDerivation) { + ut_txv_set_cur_epochs(ctx_, 9); /* steady state -> frame_count becomes 10 */ + ut_txv_set_tr_offset(ctx_, 1000.0L); + ut_txv_set_vrx(ctx_, 2); + ut_txv_set_trs(ctx_, 100.0L); + ut_txv_set_mock_ptp_time(ctx_, 10000000); + ut_txv_set_mock_tsc_time(ctx_, 500000); + + ASSERT_EQ(ut_txv_sync_pacing(ctx_, 0), 0); + + EXPECT_EQ(ut_txv_cur_epochs(ctx_), 10u); + EXPECT_DOUBLE_EQ((double)ut_txv_ptp_time_cursor(ctx_), 10000800.0); + EXPECT_DOUBLE_EQ((double)ut_txv_tsc_time_cursor(ctx_), 500800.0); + EXPECT_EQ(ut_txv_tsc_time_frame_start(ctx_), 500800u); +} + +TEST_F(St20TxSyncPacingTest, ExactUserPacingBypassesTransmissionStartTime) { + ut_txv_set_cur_epochs(ctx_, 9); + ut_txv_set_exact_user_pacing(ctx_, true); + ut_txv_set_mock_ptp_time(ctx_, 10000000); + ut_txv_set_mock_tsc_time(ctx_, 500000); + + ASSERT_EQ(ut_txv_sync_pacing(ctx_, 10500000), 0); + + EXPECT_DOUBLE_EQ((double)ut_txv_ptp_time_cursor(ctx_), + 10500000.0); /* verbatim required_tai */ + EXPECT_DOUBLE_EQ((double)ut_txv_tsc_time_cursor(ctx_), 1000000.0); +} + +/* tv_pacing_required_tai() is the frame-level-only helper that derives + * required_tai from app-supplied timestamps; EXACT_USER_PACING+timestamp=0 + * is flagged there, not in tv_sync_pacing() (which is also reached from the + * RTP-level path with required_tai=0 by design and must never flag). */ +TEST_F(St20TxSyncPacingTest, ExactUserPacingWithZeroRequiredTaiShouldBeFlagged) { + ut_txv_set_user_pacing(ctx_, true); + ut_txv_set_exact_user_pacing(ctx_, true); + + EXPECT_EQ(ut_txv_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_TAI, /*timestamp=*/0), + 0u); + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u) + << "EXACT_USER_PACING with timestamp=0 should be flagged as an invalid " + "request"; +} + +/* RTP-level callers invoke tv_sync_pacing() directly with required_tai=0, + * bypassing tv_pacing_required_tai() entirely, even when EXACT_USER_PACING + * is set (a session with type=RTP_LEVEL + USER_PACING|EXACT_USER_PACING + * passes tv_ops_check()). This must never flag on every frame. */ +TEST_F(St20TxSyncPacingTest, RtpLevelExactUserPacingWithZeroRequiredTaiNeverFlags) { + ut_txv_set_cur_epochs(ctx_, 9); + ut_txv_set_exact_user_pacing(ctx_, true); + ut_txv_set_mock_ptp_time(ctx_, 10000000); + ut_txv_set_mock_tsc_time(ctx_, 500000); + + ASSERT_EQ(ut_txv_sync_pacing(ctx_, /*required_tai=*/0), 0); + + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 0u) + << "tv_sync_pacing() alone (the RTP-level call path) must never flag " + "stat_error_user_timestamp regardless of EXACT_USER_PACING"; +} + +TEST_F(St20TxSyncPacingTest, NegativeTimeToTxClampsToZero) { + ut_txv_set_cur_epochs(ctx_, 9); /* steady state -> frame_count becomes 10 */ + ut_txv_set_tr_offset(ctx_, 0.0L); + ut_txv_set_vrx(ctx_, 2000); + ut_txv_set_trs(ctx_, 100.0L); /* vrx*trs=200000, pushes start time before cur_tai */ + ut_txv_set_mock_ptp_time(ctx_, 10000000); + ut_txv_set_mock_tsc_time(ctx_, 500000); + + ASSERT_EQ(ut_txv_sync_pacing(ctx_, 0), 0); + + EXPECT_DOUBLE_EQ((double)ut_txv_tsc_time_cursor(ctx_), + 500000.0); /* clamped: cur_tsc + 0 */ + EXPECT_EQ(ut_txv_tsc_time_frame_start(ctx_), 500000u); +} diff --git a/tests/unit/session/st20_tx_harness.c b/tests/unit/session/st20_tx_harness.c new file mode 100644 index 000000000..5e4a33107 --- /dev/null +++ b/tests/unit/session/st20_tx_harness.c @@ -0,0 +1,205 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * C harness for the ST 2110-20 (video) TX epoch/pacing math unit tests. + * + * Includes the production st_tx_video_session.c directly so the file-local + * static functions (calc_frame_count_since_epoch, tv_sync_pacing, + * validate_user_timestamp, transmission_start_time, ...) become visible in + * this translation unit. Non-static symbols duplicate those in libmtl; + * --allow-multiple-definition resolves this. USDT is disabled to avoid + * probe-semaphore link references. + * + * mt_get_tsc() is mocked with a preprocessor seam instead of any production + * source change: mt_main.h is included first, under its real name, so the + * genuine mt_get_tsc() compiles normally; its include guard then makes the + * copy pulled in transitively by st_tx_video_session.c a no-op, so the + * `#define mt_get_tsc ...` below only rewrites the call sites written inside + * st_tx_video_session.c itself. mt_get_ptp_time() needs no such seam -- it + * already dispatches through the pre-existing, production `ptp_get_time_fn` + * function-pointer field on `struct mt_interface`. + * + * Only pacing math is exercised here -- no mbuf pool, queue, or packet I/O + * is set up, since neither target function touches packets. + */ + +#include +#include + +#undef MTL_HAS_USDT +#include "common/ut_common.h" +#include "mt_main.h" +#include "st2110/st_tx_video_session.h" + +/* ── opaque context ───────────────────────────────────────────────────── */ + +struct ut_txv_ctx { + struct mtl_main_impl impl; + struct st_tx_video_session_impl session; + uint64_t mock_ptp_ns; + uint64_t mock_tsc_ns; + int notify_late_calls; + uint64_t notify_late_last_delta; +}; + +#include "session/st20_tx_harness.h" + +/* ── mocked time sources ──────────────────────────────────────────────── */ + +static uint64_t ut_txv_ptp_time_fn(struct mtl_main_impl* impl, enum mtl_port port) { + (void)port; + struct ut_txv_ctx* ctx = (struct ut_txv_ctx*)impl; /* impl is ctx's first member */ + return ctx->mock_ptp_ns; +} + +static uint64_t ut_txv_tsc_time_fn(struct mtl_main_impl* impl) { + struct ut_txv_ctx* ctx = (struct ut_txv_ctx*)impl; + return ctx->mock_tsc_ns; +} + +static int ut_txv_notify_frame_late(void* priv, uint64_t epoch_skipped) { + struct ut_txv_ctx* ctx = priv; + ctx->notify_late_calls++; + ctx->notify_late_last_delta = epoch_skipped; + return 0; +} + +/* Seam: from here on, calls to mt_get_tsc() written inside st_tx_video_session.c + * resolve to our mock instead. The real mt_get_tsc() compiled above (via the + * explicit mt_main.h include) is unaffected. */ +#define mt_get_tsc ut_txv_tsc_time_fn +#include "st2110/st_tx_video_session.c" +#undef mt_get_tsc + +/* ── init (delegates to common) ───────────────────────────────────────── */ + +int ut_txv_init(void) { + return ut_eal_init(); +} + +/* ── context create / destroy ─────────────────────────────────────────── */ + +ut_txv_ctx* ut_txv_create(void) { + ut_txv_ctx* ctx = calloc(1, sizeof(*ctx)); + if (!ctx) return NULL; + + ctx->impl.type = MT_HANDLE_MAIN; + ctx->impl.tsc_hz = rte_get_tsc_hz(); + ctx->impl.inf[MTL_PORT_P].ptp_get_time_fn = ut_txv_ptp_time_fn; + + struct st_tx_video_session_impl* s = &ctx->session; + s->impl = &ctx->impl; + s->idx = 0; + s->pacing.frame_time = 1000000.0L; /* 1ms, round number for simple math */ + s->pacing.max_onward_epochs = 3; + s->ops.notify_frame_late = ut_txv_notify_frame_late; + s->ops.priv = ctx; + + return ctx; +} + +void ut_txv_destroy(ut_txv_ctx* ctx) { + free(ctx); +} + +/* ── pacing setup ─────────────────────────────────────────────────────── */ + +void ut_txv_set_frame_time(ut_txv_ctx* ctx, long double frame_time_ns) { + ctx->session.pacing.frame_time = frame_time_ns; +} + +void ut_txv_set_max_onward_epochs(ut_txv_ctx* ctx, uint32_t max_onward_epochs) { + ctx->session.pacing.max_onward_epochs = max_onward_epochs; +} + +void ut_txv_set_cur_epochs(ut_txv_ctx* ctx, uint64_t cur_epochs) { + ctx->session.pacing.cur_epochs = cur_epochs; +} + +void ut_txv_set_tr_offset(ut_txv_ctx* ctx, long double tr_offset_ns) { + ctx->session.pacing.tr_offset = tr_offset_ns; +} + +void ut_txv_set_vrx(ut_txv_ctx* ctx, uint32_t vrx) { + ctx->session.pacing.vrx = vrx; +} + +void ut_txv_set_trs(ut_txv_ctx* ctx, long double trs_ns) { + ctx->session.pacing.trs = trs_ns; +} + +void ut_txv_set_exact_user_pacing(ut_txv_ctx* ctx, bool enable) { + if (enable) + ctx->session.ops.flags |= ST20_TX_FLAG_EXACT_USER_PACING; + else + ctx->session.ops.flags &= ~ST20_TX_FLAG_EXACT_USER_PACING; +} + +void ut_txv_set_user_pacing(ut_txv_ctx* ctx, bool enable) { + if (enable) + ctx->session.ops.flags |= ST20_TX_FLAG_USER_PACING; + else + ctx->session.ops.flags &= ~ST20_TX_FLAG_USER_PACING; +} + +void ut_txv_set_mock_ptp_time(ut_txv_ctx* ctx, uint64_t ptp_ns) { + ctx->mock_ptp_ns = ptp_ns; +} + +void ut_txv_set_mock_tsc_time(ut_txv_ctx* ctx, uint64_t tsc_ns) { + ctx->mock_tsc_ns = tsc_ns; +} + +/* ── code under test ──────────────────────────────────────────────────── */ + +uint64_t ut_txv_calc_frame_count_since_epoch(ut_txv_ctx* ctx, uint64_t cur_tai, + uint64_t required_tai) { + return calc_frame_count_since_epoch(&ctx->session, cur_tai, required_tai); +} + +int ut_txv_sync_pacing(ut_txv_ctx* ctx, uint64_t required_tai) { + return tv_sync_pacing(&ctx->impl, &ctx->session, required_tai); +} + +uint64_t ut_txv_pacing_required_tai(ut_txv_ctx* ctx, enum st10_timestamp_fmt tfmt, + uint64_t timestamp) { + return tv_pacing_required_tai(&ctx->session, tfmt, timestamp); +} + +/* ── accessors ─────────────────────────────────────────────────────────── */ + +uint64_t ut_txv_cur_epochs(const ut_txv_ctx* ctx) { + return ctx->session.pacing.cur_epochs; +} + +long double ut_txv_tsc_time_cursor(const ut_txv_ctx* ctx) { + return ctx->session.pacing.tsc_time_cursor; +} + +long double ut_txv_ptp_time_cursor(const ut_txv_ctx* ctx) { + return ctx->session.pacing.ptp_time_cursor; +} + +uint64_t ut_txv_tsc_time_frame_start(const ut_txv_ctx* ctx) { + return ctx->session.pacing.tsc_time_frame_start; +} + +uint64_t ut_txv_stat_epoch_onward(const ut_txv_ctx* ctx) { + return ctx->session.port_user_stats.common.stat_epoch_onward; +} + +uint64_t ut_txv_stat_epoch_drop(const ut_txv_ctx* ctx) { + return ctx->session.port_user_stats.common.stat_epoch_drop; +} + +uint64_t ut_txv_stat_error_user_timestamp(const ut_txv_ctx* ctx) { + return ctx->session.port_user_stats.common.stat_error_user_timestamp; +} + +int ut_txv_notify_late_calls(const ut_txv_ctx* ctx) { + return ctx->notify_late_calls; +} + +uint64_t ut_txv_notify_late_last_delta(const ut_txv_ctx* ctx) { + return ctx->notify_late_last_delta; +} diff --git a/tests/unit/session/st20_tx_harness.h b/tests/unit/session/st20_tx_harness.h new file mode 100644 index 000000000..95a743846 --- /dev/null +++ b/tests/unit/session/st20_tx_harness.h @@ -0,0 +1,78 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * C harness API for the ST 2110-20 (video) TX epoch/pacing math unit tests. + * + * Wraps a single `st_tx_video_session_impl` and exposes thin wrappers over + * the file-local `calc_frame_count_since_epoch()` and `tv_sync_pacing()`. + * Both mocked time sources (PTP and TSC) are fully controllable so tests can + * hit exact boundary values deterministically. All MTL internal types are + * kept opaque so the C++ test layer never includes libmtl headers. + */ + +#ifndef _ST20_TX_SESSION_HARNESS_H_ +#define _ST20_TX_SESSION_HARNESS_H_ + +#include +#include + +#include "st_api.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct ut_txv_ctx ut_txv_ctx; + +/* Initialise the shared DPDK EAL. Idempotent — safe to call once per gtest + * fixture SetUp(). Returns 0 on success, < 0 on failure. */ +int ut_txv_init(void); + +/* Create a context with default pacing (frame_time=1ms, max_onward_epochs=3, + * all other pacing fields zeroed). Caller owns the returned pointer and must + * free it with ut_txv_destroy(). Returns NULL on allocation failure. */ +ut_txv_ctx* ut_txv_create(void); +void ut_txv_destroy(ut_txv_ctx* ctx); + +/* ── pacing setup ──────────────────────────────────────────────────────── */ +void ut_txv_set_frame_time(ut_txv_ctx* ctx, long double frame_time_ns); +void ut_txv_set_max_onward_epochs(ut_txv_ctx* ctx, uint32_t max_onward_epochs); +void ut_txv_set_cur_epochs(ut_txv_ctx* ctx, uint64_t cur_epochs); +void ut_txv_set_tr_offset(ut_txv_ctx* ctx, long double tr_offset_ns); +void ut_txv_set_vrx(ut_txv_ctx* ctx, uint32_t vrx); +void ut_txv_set_trs(ut_txv_ctx* ctx, long double trs_ns); +/* Toggle ST20_TX_FLAG_EXACT_USER_PACING on the session's ops.flags. */ +void ut_txv_set_exact_user_pacing(ut_txv_ctx* ctx, bool enable); +/* Toggle ST20_TX_FLAG_USER_PACING on the session's ops.flags. */ +void ut_txv_set_user_pacing(ut_txv_ctx* ctx, bool enable); + +/* Mocked time sources consumed by tv_sync_pacing() via mt_get_ptp_time()/ + * mt_get_tsc(). Take effect immediately; no auto-advance between calls. */ +void ut_txv_set_mock_ptp_time(ut_txv_ctx* ctx, uint64_t ptp_ns); +void ut_txv_set_mock_tsc_time(ut_txv_ctx* ctx, uint64_t tsc_ns); + +/* ── code under test ───────────────────────────────────────────────────── */ +uint64_t ut_txv_calc_frame_count_since_epoch(ut_txv_ctx* ctx, uint64_t cur_tai, + uint64_t required_tai); +/* Drives the mocked ptp/tsc values set above; required_tai as in the + * production st20_tx_ops (0 = no user-supplied timestamp). */ +int ut_txv_sync_pacing(ut_txv_ctx* ctx, uint64_t required_tai); +uint64_t ut_txv_pacing_required_tai(ut_txv_ctx* ctx, enum st10_timestamp_fmt tfmt, + uint64_t timestamp); + +/* ── accessors ─────────────────────────────────────────────────────────── */ +uint64_t ut_txv_cur_epochs(const ut_txv_ctx* ctx); +long double ut_txv_tsc_time_cursor(const ut_txv_ctx* ctx); +long double ut_txv_ptp_time_cursor(const ut_txv_ctx* ctx); +uint64_t ut_txv_tsc_time_frame_start(const ut_txv_ctx* ctx); +uint64_t ut_txv_stat_epoch_onward(const ut_txv_ctx* ctx); +uint64_t ut_txv_stat_epoch_drop(const ut_txv_ctx* ctx); +uint64_t ut_txv_stat_error_user_timestamp(const ut_txv_ctx* ctx); +int ut_txv_notify_late_calls(const ut_txv_ctx* ctx); +uint64_t ut_txv_notify_late_last_delta(const ut_txv_ctx* ctx); + +#ifdef __cplusplus +} +#endif + +#endif /* _ST20_TX_SESSION_HARNESS_H_ */ From 22717f8305fcd5c05e01d128e90be523797895c8 Mon Sep 17 00:00:00 2001 From: "Kasiewicz, Marek" Date: Tue, 7 Jul 2026 08:53:24 +0000 Subject: [PATCH 06/19] Test: Add NoCtx regression test for PTP epoch recovery Simulates a PHC seeded ahead of the true grandmaster and later corrected by the servo, reproducing the epoch/pacing stuck-recovery bug fixed in st_tx_video_session.c. Parametrized over every st21_tx_pacing_way, since calc_frame_count_since_epoch() runs identically regardless of pacing way. Signed-off-by: Kasiewicz, Marek --- tests/integration_tests/noctx/meson.build | 1 + .../st20p_ptp_epoch_recovery_tests.cpp | 117 ++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 tests/integration_tests/noctx/testcases/st20p_ptp_epoch_recovery_tests.cpp diff --git a/tests/integration_tests/noctx/meson.build b/tests/integration_tests/noctx/meson.build index 9d145ad07..f4abe8c4f 100644 --- a/tests/integration_tests/noctx/meson.build +++ b/tests/integration_tests/noctx/meson.build @@ -18,6 +18,7 @@ noctx_sources = files( 'testcases/st20p_user_pacing_tests.cpp', 'testcases/st20p_user_timestamp_tests.cpp', 'testcases/st20p_stability_tests.cpp', + 'testcases/st20p_ptp_epoch_recovery_tests.cpp', 'testcases/st40i_tests.cpp', 'testcases/st40p_auto_detect_tests.cpp', 'testcases/st40p_user_pacing_tests.cpp', diff --git a/tests/integration_tests/noctx/testcases/st20p_ptp_epoch_recovery_tests.cpp b/tests/integration_tests/noctx/testcases/st20p_ptp_epoch_recovery_tests.cpp new file mode 100644 index 000000000..55a96c3b0 --- /dev/null +++ b/tests/integration_tests/noctx/testcases/st20p_ptp_epoch_recovery_tests.cpp @@ -0,0 +1,117 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2025 Intel Corporation + */ + +#include +#include +#include + +#include "core/constants.hpp" +#include "core/test_fixture.hpp" +#include "handlers/st20p_handler.hpp" + +namespace { + +std::atomic g_epoch_recovery_start_ns{0}; +std::atomic g_epoch_recovery_future_offset_ns{0}; + +uint64_t EpochRecoveryMonotonicNowNs() { + struct timespec spec; + clock_gettime(CLOCK_MONOTONIC, &spec); + return (uint64_t)spec.tv_sec * NS_PER_S + spec.tv_nsec; +} + +/* Simulates a PHC seeded from the host system clock (ahead of the true PTP + * grandmaster) that later gets corrected by the PTP servo. Returns + * elapsed-since-first-call plus a controllable offset the test steps to 0 + * mid-run to emulate that correction. */ +uint64_t EpochRecoveryPtpClockNow(void* priv) { + (void)priv; + uint64_t start = g_epoch_recovery_start_ns.load(std::memory_order_acquire); + if (!start) { + start = EpochRecoveryMonotonicNowNs(); + g_epoch_recovery_start_ns.store(start, std::memory_order_release); + } + int64_t offset = g_epoch_recovery_future_offset_ns.load(std::memory_order_acquire); + return (EpochRecoveryMonotonicNowNs() - start) + offset; +} + +/* Shared repro body: calc_frame_count_since_epoch() (the epoch/onward-gap + * math under test) runs identically for every st21_tx_pacing_way -- the + * pacing way only changes how packets are scheduled onto the wire, not how + * the epoch catch-up gap is computed. Parametrizing over pacing_way confirms + * (or disproves) that the bug is pacing-way-specific rather than TSN-only. */ +void RunEpochOnwardRecoveryCase(NoCtxTest* self, struct st_tests_context* ctx, + enum st21_tx_pacing_way pacing_way) { + /* Offset must exceed pacing->max_onward_epochs (~1s of frames) so the + * post-step gap actually trips stat_epoch_onward instead of being + * absorbed as ordinary jitter. */ + constexpr int64_t kFutureOffsetNs = 3 * (int64_t)NS_PER_S; + g_epoch_recovery_start_ns.store(0, std::memory_order_release); + g_epoch_recovery_future_offset_ns.store(kFutureOffsetNs, std::memory_order_release); + + ctx->para.pacing = pacing_way; + ctx->para.ptp_get_time_fn = EpochRecoveryPtpClockNow; + ctx->para.log_level = MTL_LOG_LEVEL_INFO; + ctx->para.flags &= ~MTL_FLAG_DEV_AUTO_START_STOP; + /* Shorten the periodic "M T DEV STATE" stat dump (default 10s) so the run + * below comfortably covers several dumps without needing a long sleep. */ + ctx->para.dump_period_s = 2; + ctx->handle = mtl_init(&ctx->para); + ASSERT_TRUE(ctx->handle != nullptr); + + /* No frame-content/timestamp strategy: St20pDefaultTimestamp assumes a + * steady real-time-correlated clock, which this test deliberately violates + * to reproduce the bug. Only stat_epoch_onward is asserted on below. */ + auto bundle = + self->createSt20pHandlerBundle(/*createTx=*/true, /*createRx=*/true, nullptr); + + bundle.handler->startSession(); + mtl_start(ctx->handle); + + /* let the session settle and transmit for a while against the inflated clock */ + std::this_thread::sleep_for(std::chrono::seconds(6)); + + /* simulate the PTP servo correcting the clock back to the true grandmaster time */ + g_epoch_recovery_future_offset_ns.store(0, std::memory_order_release); + + struct st20_tx_user_stats stats = {}; + uint64_t last_onward = 0; + bool recovered = false; + + /* a correct fix resyncs on the first frame after the step, so the delta + * should hit 0 by the poll right after the one that observes the resync */ + for (int i = 0; i < 3; i++) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + ASSERT_EQ(st20p_tx_get_session_stats(bundle.handler->sessionsHandleTx, &stats), 0); + uint64_t delta = stats.common.stat_epoch_onward - last_onward; + last_onward = stats.common.stat_epoch_onward; + if (delta == 0) { + recovered = true; + break; + } + } + + bundle.handler->stopSession(); + + EXPECT_TRUE(recovered) + << "stat_epoch_onward kept incrementing after the simulated PTP correction " + "(cumulative value " + << last_onward + << ") -- TX pacing never recovered from an epoch initialized ahead of real time"; +} + +} /* namespace */ + +/* TSN pacing (launch-time offload) is only advertised by PF drivers; this + * test requires the noctx port pair to be bound as PF, not VF. */ +TEST_F(NoCtxTest, st20p_tx_epoch_onward_recovers_after_ptp_step) { + RunEpochOnwardRecoveryCase(this, ctx, ST21_TX_PACING_WAY_TSN); +} + +/* Same repro with software TSC-based pacing (no NIC launch-time offload, so + * this pacing way also runs on VF ports) -- confirms the epoch-onward gap + * never recovers regardless of pacing way, i.e. this is not a TSN-only bug. */ +TEST_F(NoCtxTest, st20p_tx_epoch_onward_recovers_after_ptp_step_tsc_pacing) { + RunEpochOnwardRecoveryCase(this, ctx, ST21_TX_PACING_WAY_TSC); +} From 0396893f2bf558331a1e5749bdfb1b56f05b26f3 Mon Sep 17 00:00:00 2001 From: "Kasiewicz, Marek" Date: Tue, 7 Jul 2026 08:53:45 +0000 Subject: [PATCH 07/19] Test: Support 2-port/PF runs for NoCtx tests Most NoCtxTest cases only need a single TX/RX port pair; only the redundant-session tests need all 4. Requiring 4 ports up front made it impossible to run the suite (including TSN/launch-time-pacing tests, which need a PF rather than a VF) on hosts with just 2 ports bound. run.sh keeps requiring all 4 TEST_PORT_1..4 slots, since silently building --port_list from whichever ports happen to be set left the redundant-session tests failing individually and ambiguously instead of the script refusing to run up front. Add a dedicated run_pf.sh for the PF-only TSN/launch-time-pacing case: it requires just TEST_PF_PORT_1/2 and filters to NoCtxTest cases named with a `_pf_` infix, so the two port sets are never conflated. Rename the TSN pacing test to st20p_tx_epoch_onward_recovers_after_ptp_step_pf_tsn_pacing to match. TSN launch-time pacing is currently only validated on Intel E830 (PCI device 12d2); note this in noctx.md rather than gating on device ID in the test itself. Mirror the same split in the MCP server: run_noctx_tests() keeps the strict 4-port requirement and excludes `_pf_` tests, and a new run_noctx_pf_tests() tool auto-discovers 2 PF ports (absence of a physfn symlink) and runs the `_pf_` tests against them. Its gtest_filter enumerates by the caller's substring first and requires `_pf_` as a separate post-filter, instead of a single glob that assumed `_pf_` always precedes the caller's filter text -- the naming convention puts the descriptive prefix first (e.g. `st20p_..._pf_tsn_pacing`), so that glob ordering could never match a filter on that prefix. Signed-off-by: Kasiewicz, Marek --- .../mtl-system-setup.instructions.md | 1 + .github/mcp/mtl_mcp_server.py | 218 ++++++++++++++++-- tests/integration_tests/noctx/noctx.md | 40 +++- tests/integration_tests/noctx/run.sh | 11 +- tests/integration_tests/noctx/run_pf.sh | 98 ++++++++ .../st20p_ptp_epoch_recovery_tests.cpp | 10 +- 6 files changed, 345 insertions(+), 33 deletions(-) create mode 100755 tests/integration_tests/noctx/run_pf.sh diff --git a/.github/instructions/mtl-system-setup.instructions.md b/.github/instructions/mtl-system-setup.instructions.md index 0c77efa8b..56d3345b5 100644 --- a/.github/instructions/mtl-system-setup.instructions.md +++ b/.github/instructions/mtl-system-setup.instructions.md @@ -53,6 +53,7 @@ decision logic and domain knowledge that tool descriptions alone don't convey. - **ldconfig**: Always run after installing DPDK or MTL — libraries won't be found otherwise. - **MtlManager**: Must be running for lcore allocation. Start with `manager_start`. - **E810 vs E830**: Both supported, same VF workflow. E830 device ID `12d2`, E810 device ID `1592`. +- **NoCtx `_pf_` tests (TSN/launch-time-pacing)**: only validated on E830 (`12d2`). Not gated by device ID in the test code — check the port's device ID yourself (e.g. `cat /sys/bus/pci/devices//device`) before running `run_pf.sh` / `run_noctx_pf_tests`; expect unrelated failures on E810 or other NICs. - **NUMA**: Allocate from the same NUMA node as the NIC for best performance. VFs inherit PF's NUMA node. - **VF BDF patterns**: PF `.0` creates VFs at `:01.0-5`, PF `.1` at `:11.0-5`. - **versions.env**: Defines `DPDK_VER`, `ICE_VER`, `ICE_DMID`. Patches live in `patches/dpdk//`. diff --git a/.github/mcp/mtl_mcp_server.py b/.github/mcp/mtl_mcp_server.py index 0eb4df21e..2abf1de80 100644 --- a/.github/mcp/mtl_mcp_server.py +++ b/.github/mcp/mtl_mcp_server.py @@ -1541,44 +1541,48 @@ def run_noctx_tests( invokes one process per test, sleeping `cooldown_seconds` between them (mirrors tests/integration_tests/noctx/run.sh). + All 4 ports are required — this is the full-suite run and a handful of + tests exercise redundant (4-port) sessions; requiring fewer here would let + those tests fail ambiguously depending on how many ports happen to be + supplied. Tests named with a `_pf_` infix (e.g. TSN/launch-time-pacing) + require a PF, which this VF-oriented run can never satisfy, so they are + excluded automatically — use `run_noctx_pf_tests` for those instead. + Args: - port_1..port_4: VF BDFs. Auto-discovered (vfio-pci) if any are empty. + port_1, port_2, port_3, port_4: BDFs for the 4 ports. Auto-discovered + (any vfio-pci-bound ports, PF or VF) if left empty. gtest_filter: Gtest filter scoped to NoCtxTest (e.g. '*nonsplit*' or 'NoCtxTest.st40i_*'). All NoCtxTest cases if empty. timeout_seconds: Max seconds per individual test process (default 600). cooldown_seconds: Sleep between tests (default 10). """ - # Auto-discover - if not all([port_1, port_2, port_3, port_4]): - vfs = ( + # Auto-discover only if the caller supplied nothing; otherwise respect + # exactly what was passed. + if not any([port_1, port_2, port_3, port_4]): + discovered = ( _run_output( "dpdk-devbind.py -s 2>/dev/null | awk '/drv=vfio-pci/{print $1}' | head -4" ) .strip() .splitlines() ) - if len(vfs) < 4: - return ( - "Error: noctx tests require 4 VF ports bound to vfio-pci. " - f"Found {len(vfs)}. Run `setup_after_reboot_auto` first." - ) - ports = [v.strip() for v in vfs[:4]] - port_1 = port_1 or ports[0] - port_2 = port_2 or ports[1] - port_3 = port_3 or ports[2] - port_4 = port_4 or ports[3] - - # Validate all BDFs - for label, bdf in [ - ("port_1", port_1), - ("port_2", port_2), - ("port_3", port_3), - ("port_4", port_4), - ]: + ports = [v.strip() for v in discovered[:4]] + else: + ports = [p for p in (port_1, port_2, port_3, port_4) if p] + + if len(ports) < 4: + return ( + "Error: noctx full-suite run requires 4 ports bound to vfio-pci " + f"(PF or VF). Found {len(ports)}. Run `setup_after_reboot_auto` first, " + "or supply port_1..port_4 explicitly." + ) + + # Validate the BDFs we ended up with + for idx, bdf in enumerate(ports, start=1): err = _validate_bdf(bdf) if err: - return f"Error: {label} — {err}" + return f"Error: port_{idx} — {err}" # Sanitize gtest_filter if gtest_filter and not re.match(r"^[a-zA-Z0-9_.*:/-]+$", gtest_filter): @@ -1590,7 +1594,7 @@ def run_noctx_tests( sim_hint = "" if _build_has_simulate_drops() else _SIM_DROPS_HINT - port_list = f"{port_1},{port_2},{port_3},{port_4}" + port_list = ",".join(ports) # Default to all NoCtxTest cases; scope user filter to NoCtxTest.* if not gtest_filter: @@ -1600,6 +1604,12 @@ def run_noctx_tests( else: list_filter = f"NoCtxTest.*{gtest_filter}*" + # PF-only tests (name contains "_pf_") can never pass on these VF-capable + # ports; exclude them unless the caller already wrote their own negative + # pattern (gtest filter syntax only supports one "-" exclusion group). + if "-" not in list_filter: + list_filter = f"{list_filter}-NoCtxTest.*_pf_*" + # Enumerate matching test names (one process per test below) listing = _run_output( f"{binary} --gtest_list_tests --no_ctx" @@ -1673,7 +1683,165 @@ def run_noctx_tests( return ( f"## Noctx Test Results\n" f"- Status: {status}\n" - f"- Ports: {port_1}, {port_2}, {port_3}, {port_4}\n" + f"- Ports: {port_list}\n" + f"- Tests run: {len(results)} (passed {passed}, failed {failed})\n" + f"- Full log: `{log_path}`\n" + f"\n### Per-test results\n{per_test}\n" + f"{last_failure}" + f"{sim_hint}" + ) + + +@mcp.tool() +def run_noctx_pf_tests( + port_1: str = "", + port_2: str = "", + gtest_filter: str = "", + timeout_seconds: int = 600, + cooldown_seconds: int = 10, +) -> str: + """ + Run the NoCtxTest cases that require PF ports (mirrors + tests/integration_tests/noctx/run_pf.sh). + + Tests named with a `_pf_` infix (e.g. TSN/launch-time-pacing) require a PF + port — that offload is not advertised by VF drivers — so `run_noctx_tests` + excludes them. This tool runs just those, against 2 PF-bound ports (all + current PF-only tests use a single TX/RX pair). + + Args: + port_1, port_2: BDFs for a PF TX/RX pair. Auto-discovered (vfio-pci + ports without a `physfn` sysfs link, i.e. real PFs, + not VFs) if left empty. + gtest_filter: Substring/glob to match against the test name (e.g. + 'st20p_tx_epoch_onward_recovers_after_ptp_step' or + 'st20p_*'), matched anywhere in the name — no need to + include "_pf_" yourself, it's required separately. + All `_pf_` cases if empty. + timeout_seconds: Max seconds per individual test process (default 600). + cooldown_seconds: Sleep between tests (default 10). + """ + + if not any([port_1, port_2]): + discovered = ( + _run_output( + "for d in $(dpdk-devbind.py -s 2>/dev/null | awk '/drv=vfio-pci/{print $1}'); do " + '[ ! -e "/sys/bus/pci/devices/$d/physfn" ] && echo "$d"; done | head -2' + ) + .strip() + .splitlines() + ) + ports = [v.strip() for v in discovered[:2]] + else: + ports = [p for p in (port_1, port_2) if p] + + if len(ports) < 2: + return ( + "Error: PF noctx tests require 2 ports bound to vfio-pci as PFs " + f"(not VFs). Found {len(ports)}. Run `nic_bind_pmd` on a PF, or " + "supply port_1/port_2 explicitly." + ) + + for idx, bdf in enumerate(ports, start=1): + err = _validate_bdf(bdf) + if err: + return f"Error: port_{idx} — {err}" + + if gtest_filter and not re.match(r"^[a-zA-Z0-9_.*:/-]+$", gtest_filter): + return "Error: invalid gtest_filter characters" + + binary = REPO_ROOT / "build/tests/KahawaiTest" + if not binary.is_file(): + return "Error: KahawaiTest not built. Run `build_mtl` first." + + sim_hint = "" if _build_has_simulate_drops() else _SIM_DROPS_HINT + + port_list = ",".join(ports) + + # Scope to NoCtxTest, same convention as run_noctx_tests: don't assume + # where "_pf_" falls relative to the caller's filter text (the naming + # convention puts the descriptive prefix *before* "_pf_", e.g. + # "st20p_..._pf_tsn_pacing", so a filter matching that prefix would never + # match a glob that requires "_pf_" to come first). Enumerate with just + # the caller's filter, then require "_pf_" as a separate post-filter. + if not gtest_filter: + list_filter = "NoCtxTest.*" + elif gtest_filter.startswith("NoCtxTest."): + list_filter = gtest_filter + else: + list_filter = f"NoCtxTest.*{gtest_filter}*" + + listing = _run_output( + f"{binary} --gtest_list_tests --no_ctx" + f" --port_list={port_list}" + f" --gtest_filter={list_filter}", + timeout=60, + ) + test_names: list[str] = [] + current_suite = "" + for raw in listing.splitlines(): + if not raw or raw.startswith(("DISABLED", "Note:")): + continue + if not raw.startswith(" "): + current_suite = raw.strip().rstrip(".") + else: + name = raw.strip() + if name and current_suite == "NoCtxTest" and "_pf_" in name: + test_names.append(name) + + if not test_names: + return ( + f"No PF-only (name contains '_pf_') NoCtxTest cases matched filter " + f"'{list_filter}'.\nListing output:\n{listing}" + ) + + sections: list[str] = [] + results: list[tuple[str, str]] = [] # (name, PASS|FAIL|TIMEOUT) + for idx, name in enumerate(test_names): + full = f"NoCtxTest.{name}" + out = _run_output( + f"{binary} --auto_start_stop" + f" --port_list={port_list}" + f" --gtest_filter={full}" + f" --no_ctx_tests", + timeout=timeout_seconds, + ) + if "*** TIMEOUT" in out: + status = "TIMEOUT" + elif re.search(r"\[\s+PASSED\s+\]\s+1\s+test", out): + status = "PASS" + else: + status = "FAIL" + results.append((name, status)) + sections.append(f"===== {full}: {status} =====\n{out}\n") + if idx < len(test_names) - 1 and cooldown_seconds > 0: + time.sleep(cooldown_seconds) + + combined = "\n".join(sections) + log_path = _save_test_log("noctx_pf", combined) + + passed = sum(1 for _, s in results if s == "PASS") + failed = sum(1 for _, s in results if s != "PASS") + status = "PASSED" if failed == 0 else "FAILED" + + per_test = "\n".join(f"- {s}: NoCtxTest.{n}" for n, s in results) + last_failure = "" + for name, st in results: + if st != "PASS": + for sec in sections: + if sec.startswith(f"===== NoCtxTest.{name}:"): + tail = "\n".join(sec.splitlines()[-30:]) + last_failure = ( + f"\n### First failure: NoCtxTest.{name} (last 30 lines)\n" + f"```\n{tail}\n```" + ) + break + break + + return ( + f"## Noctx PF Test Results\n" + f"- Status: {status}\n" + f"- Ports: {port_list}\n" f"- Tests run: {len(results)} (passed {passed}, failed {failed})\n" f"- Full log: `{log_path}`\n" f"\n### Per-test results\n{per_test}\n" diff --git a/tests/integration_tests/noctx/noctx.md b/tests/integration_tests/noctx/noctx.md index 9c232f650..170d02d58 100644 --- a/tests/integration_tests/noctx/noctx.md +++ b/tests/integration_tests/noctx/noctx.md @@ -17,27 +17,48 @@ and need special configuration. ## Running no context tests +Most tests only need a TX/RX port pair, but `run.sh` (the full-suite runner) +always requires all 4 ports, since a handful of redundant-session tests need +them — running with fewer ports would otherwise let those tests fail +ambiguously. Ports can be PFs or VFs bound to vfio-pci. + +TSN / launch-time-pacing tests specifically require a PF, since that offload +is not advertised by VF drivers. Such tests are named with a `_pf_` infix +(e.g. `st20p_tx_epoch_onward_recovers_after_ptp_step_pf_tsn_pacing`) so they +can be selected or excluded with a plain `--gtest_filter` wildcard. `run.sh` +excludes them (they can never pass against its VF-capable ports); use +`run_pf.sh` to run just them against PF ports instead. + +TSN launch-time-pacing has only been validated on Intel **E830** (PCI device +ID `0x12d2`); other NICs (e.g. E810, `0x1592`) may not advertise it or may +behave differently. `_pf_` tests are not gated on device ID in the test code +itself — run them on an E830 PF, and expect unrelated failures/hangs on other +hardware. + ### Command-Line ```bash ./build/tests/KahawaiTest \ --auto_start_stop \ - --port_list=0000:xx:xx.x,0000:xx:xx.x,0000:xx:xx.x,0000:xx:xx.x \ + --port_list=0000:xx:xx.x,0000:yy:yy.y \ --gtest_filter=noctx.TestName \ --no_ctx ``` -Replace `TestName` with any test from [`noctx.env`](noctx.env) and update the port addresses to match your system. +Replace `TestName` with any test from [`noctx.env`](noctx.env) and update the port addresses to match your system. Add two more comma-separated BDFs to `--port_list` for tests that need redundant sessions. ### Environment configuration -You can store the PCI addresses once in `tests/integration_tests/noctx/noctx.env`; `run.sh` sources this file automatically before executing tests. +You can store the PCI addresses once in `tests/integration_tests/noctx/noctx.env`; `run.sh` and `run_pf.sh` source this file automatically before executing tests. ```bash TEST_PORT_1=0000:xx:xx.x TEST_PORT_2=0000:yy:yy.y TEST_PORT_3=0000:zz:zz.z TEST_PORT_4=0000:aa:aa.a +# Only needed by run_pf.sh, must be bound to vfio-pci as PFs (not VFs) +TEST_PF_PORT_1=0000:bb:bb.b +TEST_PF_PORT_2=0000:cc:cc.c ``` Exporting the same variables in your shell will continue to work, but keeping them in `noctx.env` avoids repeating the configuration. @@ -51,5 +72,16 @@ issues you can use a handy script run.sh ``` -to run all nocx tests +to run all tests that don't need a PF (all `TEST_PORT_1..4` must be set). + +### Running PF-only tests + +Tests named with a `_pf_` infix require PF ports and are skipped by `run.sh`. +Run them separately with: +```bash +run_pf.sh +``` + +which requires `TEST_PF_PORT_1`/`TEST_PF_PORT_2` (2 PF ports covers all +current PF-only tests). diff --git a/tests/integration_tests/noctx/run.sh b/tests/integration_tests/noctx/run.sh index 4c6cceb9d..388154a3f 100755 --- a/tests/integration_tests/noctx/run.sh +++ b/tests/integration_tests/noctx/run.sh @@ -44,7 +44,14 @@ if [ -z "$TEST_PORT_1" ] || [ -z "$TEST_PORT_2" ] || [ -z "$TEST_PORT_3" ] || [ exit 1 fi -test_names=$("$BUILD_PATH" --gtest_list_tests --no_ctx --port_list="${TEST_PORT_1},${TEST_PORT_2},${TEST_PORT_3},${TEST_PORT_4}" --gtest_filter="NoCtxTest.${NOCTX_FILTER}*" 2>/dev/null | +PORT_LIST="${TEST_PORT_1},${TEST_PORT_2},${TEST_PORT_3},${TEST_PORT_4}" + +# PF-only tests (name contains "_pf_", e.g. TSN/launch-time-pacing) can +# never pass against these VF ports; exclude them explicitly instead of +# letting them fail for the wrong reason. Run them via run_pf.sh instead. +GTEST_FILTER="NoCtxTest.${NOCTX_FILTER}*-NoCtxTest.*_pf_*" + +test_names=$("$BUILD_PATH" --gtest_list_tests --no_ctx --port_list="${PORT_LIST}" --gtest_filter="${GTEST_FILTER}" 2>/dev/null | awk '/^ [a-zA-Z]/ {gsub(/^ /, ""); print}') # Use TMP_FOLDER from environment or fallback to /tmp @@ -64,7 +71,7 @@ while IFS= read -r test_name || [ -n "$test_name" ]; do if "$BUILD_PATH" \ --auto_start_stop \ - --port_list="${TEST_PORT_1},${TEST_PORT_2},${TEST_PORT_3},${TEST_PORT_4}" \ + --port_list="${PORT_LIST}" \ --gtest_filter="NoCtxTest.$test_name" \ --gtest_output="xml:${xml_file}" \ --no_ctx_tests; then diff --git a/tests/integration_tests/noctx/run_pf.sh b/tests/integration_tests/noctx/run_pf.sh new file mode 100755 index 000000000..d762dcaaa --- /dev/null +++ b/tests/integration_tests/noctx/run_pf.sh @@ -0,0 +1,98 @@ +#!/bin/bash + +# SPDX-License-Identifier: BSD-3-Clause +# Copyright 2026 Intel Corporation + +# Runs the NoCtxTest cases that require PF ports (name contains "_pf_", e.g. +# TSN/launch-time-pacing offload -- only advertised by PF drivers, not VF). +# run.sh excludes these from its VF-based full run; use this script to run +# just them, against a pair of ports bound as PF to vfio-pci. +# +# Most PF-only tests only need a single TX/RX pair, so 2 ports are enough. + +: "${EXIT_ON_FAILURE:=1}" + +script_name=$(basename "${BASH_SOURCE[0]}") +script_path=$(readlink -qe "${BASH_SOURCE[0]}") +script_folder=${script_path/$script_name/} +# shellcheck disable=SC1091 +. "${script_folder}/../../../script/common.sh" +cd "${script_folder}" || exit 1 + +mtl_folder="${script_folder}/../../.." + +# Detect whether to use .local_install (CI) or local build paths +if [ -z "${BUILD_PATH:-}" ]; then + if [ -d "${mtl_folder}/.local_install" ]; then + BUILD_PATH="${mtl_folder}/.local_install/mtl/bin/KahawaiTest" + else + BUILD_PATH="${mtl_folder}/build/tests/KahawaiTest" + fi +fi +ENV_FILE="${script_folder}/noctx.env" + +if [ -f "$ENV_FILE" ]; then + # shellcheck disable=SC1090 + . "$ENV_FILE" +fi + +if [ ! -f "$BUILD_PATH" ]; then + echo "Error: KahawaiTest binary not found at $BUILD_PATH" + echo "Please build the project first" + exit 1 +fi + +if [ -z "$TEST_PF_PORT_1" ] || [ -z "$TEST_PF_PORT_2" ]; then + echo "Error: TEST_PF_PORT_1/TEST_PF_PORT_2 environment variables are not set" + echo "These must be bound to vfio-pci as PFs, not VFs -- TSN/launch-time" + echo "pacing offload is only advertised by PF drivers." + echo "TEST_PF_PORT_1=$TEST_PF_PORT_1" + echo "TEST_PF_PORT_2=$TEST_PF_PORT_2" + exit 1 +fi + +PORT_LIST="${TEST_PF_PORT_1},${TEST_PF_PORT_2}" + +test_names=$("$BUILD_PATH" --gtest_list_tests --no_ctx --port_list="${PORT_LIST}" --gtest_filter="NoCtxTest.${NOCTX_FILTER}*_pf_*" 2>/dev/null | + awk '/^ [a-zA-Z]/ {gsub(/^ /, ""); print}') + +if [ -z "$test_names" ]; then + echo "No PF-only NoCtx tests found (none match *_pf_*)." + exit 0 +fi + +# Use TMP_FOLDER from environment or fallback to /tmp +: "${TMP_FOLDER:=/tmp}" +XML_OUTPUT_DIR="${TMP_FOLDER}" +mkdir -p "$XML_OUTPUT_DIR" + +test_count=0 +while IFS= read -r test_name || [ -n "$test_name" ]; do + if [[ -z "$test_name" || "$test_name" == \#* ]]; then + continue + fi + echo "Checking test: NoCtxTest.$test_name" + + test_count=$((test_count + 1)) + xml_file="${XML_OUTPUT_DIR}/noctx_pf_${test_count}.xml" + + if "$BUILD_PATH" \ + --auto_start_stop \ + --port_list="${PORT_LIST}" \ + --gtest_filter="NoCtxTest.$test_name" \ + --gtest_output="xml:${xml_file}" \ + --no_ctx_tests; then + echo "Test NoCtxTest.$test_name passed" + else + echo "Test NoCtxTest.$test_name failed with exit code $?" + if [ "$EXIT_ON_FAILURE" -eq 1 ]; then + echo "Exiting due to test failure." + exit 1 + fi + fi + + sleep 10 +done < <(echo "$test_names") + +echo "All PF-only noctx tests completed. XML files saved in $XML_OUTPUT_DIR" +echo "Total test count: $test_count" diff --git a/tests/integration_tests/noctx/testcases/st20p_ptp_epoch_recovery_tests.cpp b/tests/integration_tests/noctx/testcases/st20p_ptp_epoch_recovery_tests.cpp index 55a96c3b0..3b927905b 100644 --- a/tests/integration_tests/noctx/testcases/st20p_ptp_epoch_recovery_tests.cpp +++ b/tests/integration_tests/noctx/testcases/st20p_ptp_epoch_recovery_tests.cpp @@ -104,8 +104,14 @@ void RunEpochOnwardRecoveryCase(NoCtxTest* self, struct st_tests_context* ctx, } /* namespace */ /* TSN pacing (launch-time offload) is only advertised by PF drivers; this - * test requires the noctx port pair to be bound as PF, not VF. */ -TEST_F(NoCtxTest, st20p_tx_epoch_onward_recovers_after_ptp_step) { + * test requires the noctx port pair to be bound as PF, not VF. The "_pf_" + * infix lets run.sh/run_pf.sh select/exclude it with a plain gtest_filter + * wildcard instead of parsing the test source. + * + * TSN launch-time-pacing offload has only been validated on Intel E830 (PCI + * device 0x12d2); other NICs may not advertise it or may behave differently. + * Run this only on E830 PF ports. */ +TEST_F(NoCtxTest, st20p_tx_epoch_onward_recovers_after_ptp_step_pf_tsn_pacing) { RunEpochOnwardRecoveryCase(this, ctx, ST21_TX_PACING_WAY_TSN); } From 63a73ea6f5bd8346c8525e2624f6f69e2e5ad142 Mon Sep 17 00:00:00 2001 From: "Kasiewicz, Marek" Date: Tue, 7 Jul 2026 10:25:17 +0000 Subject: [PATCH 08/19] Fix: Require ptp sync with phc for TSN launch-time pacing TSN launch-time pacing compares each packet's launch timestamp against the NIC's hardware clock (PHC). Selecting TSN pacing without the built-in PTP service enabled, or with it forced to the TSC source, leaves the timestamp in an unrelated clock domain, so the NIC releases packets at the wrong time. Reject TSN pacing selection in that case at session init instead. Document the full set of TSN launch-time pacing requirements (E830-only capability, PF-only, ptp-synced phc) in design.md. Signed-off-by: Kasiewicz, Marek --- doc/design.md | 8 ++++++++ lib/src/dev/mt_dev.c | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/doc/design.md b/doc/design.md index f60a05d26..723fae67e 100644 --- a/doc/design.md +++ b/doc/design.md @@ -229,6 +229,14 @@ In the case that the rate-limiting feature is unavailable, TSC (Timestamp Counte ![TX Pacing](png/tx_pacing.png) +#### 4.3.3. TSN launch-time pacing + +TSN launch-time pacing (`--pacing_way tsn` / `ST21_TX_PACING_WAY_TSN`) offloads packet pacing to the NIC: MTL stamps each packet with a launch time and the NIC releases it once its internal clock (the PHC) reaches that timestamp. This has three requirements, all enforced at session init: + +- The NIC must be an Intel® E830 Series Ethernet Adapter. E810 lacks the TxPP launch-time hardware engine, so it never advertises the `RTE_ETH_TX_OFFLOAD_SEND_ON_TIMESTAMP` capability. +- The port must be a PF (Physical Function). VFs do not support the hardware timesync feature, so there is no HW PHC to compare the launch time against. +- The built-in PTP service must be enabled (`--ptp` / `MTL_FLAG_PTP_ENABLE`), and not forced to the TSC source (`MTL_FLAG_PTP_SOURCE_TSC`). The packet launch time is only meaningful if it is expressed in the same, PHC-synced clock domain the NIC compares it against. See [PTP Setup](run.md#71-ptp-setup) for how to enable it. + ### 4.4. ST2110 RX The RX (Receive) packet classification in MTL includes two types: Flow Director and RSS (Receive Side Scaling). Flow Director is preferred if the NIC is capable, as it can directly feed the desired packet into the RX session packet handling function. diff --git a/lib/src/dev/mt_dev.c b/lib/src/dev/mt_dev.c index a108a9f55..384dc705a 100644 --- a/lib/src/dev/mt_dev.c +++ b/lib/src/dev/mt_dev.c @@ -1448,6 +1448,15 @@ static int dev_if_init_pacing(struct mt_interface* inf) { err("%s(%d), this port not support tsn launch time\n", __func__, port); return -EINVAL; } + /* tsn launch time is compared against the phc, the packet timestamp must be + * phc-synced: only the built-in ptp service disciplines the phc when PF is used + */ + if (mt_user_ptp_tsc_source(inf->parent) || !mt_user_ptp_service(inf->parent)) { + err("%s(%d), tsn pacing requires ptp synced with phc, enable MTL_FLAG_PTP_ENABLE " + "(--ptp)\n", + __func__, port); + return -EINVAL; + } } return 0; From a06dd51d73395faed619b4bd43ed08fd1fa4ed04 Mon Sep 17 00:00:00 2001 From: "Kasiewicz, Marek" Date: Tue, 7 Jul 2026 10:25:31 +0000 Subject: [PATCH 09/19] Docs: Note --ptp limitation on E810/E830 secondary ports The built-in PTP service disciplines the hardware clock of the primary port only. If MTL doesn't open/use that primary port (for example, only a secondary port is passed to mtl_init), --ptp has no effect on the secondary port's clock. Signed-off-by: Kasiewicz, Marek --- doc/chunks/_ptp_setup.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/chunks/_ptp_setup.md b/doc/chunks/_ptp_setup.md index dbf4dee7b..02861d8d7 100644 --- a/doc/chunks/_ptp_setup.md +++ b/doc/chunks/_ptp_setup.md @@ -27,3 +27,5 @@ This project includes built-in support for the Precision Time Protocol (PTP) pro To enable this feature in the RxTxApp sample application, use the `--ptp` argument. The control for the built-in PTP feature is the `MTL_FLAG_PTP_ENABLE` flag in the `mtl_init_params` structure. Note: Currently, the VF (Virtual Function) does not support the hardware timesync feature. Therefore, for VF deployment, the timestamp of the transmitted (TX) and received (RX) packets is read from the CPU TSC (TimeStamp Counter) instead. In this case, it is not possible to obtain a stable delta in the PTP adjustment, and the maximum accuracy achieved will be up to 1us. + +Note: For Intel® E810 and E830 Series Ethernet Adapters, the built-in PTP relies on the hardware timesync feature of the primary port (port 0) of the NIC. If the primary port is not opened/used by MTL (for example, only a secondary port is passed to `mtl_init`), `--ptp` will not work on the secondary port. From 3888ef8d040064adab104cc281b88907aa2d5f10 Mon Sep 17 00:00:00 2001 From: "Kasiewicz, Marek" Date: Tue, 7 Jul 2026 10:37:38 +0000 Subject: [PATCH 10/19] Test: Add st20p pacing_way coverage across VF and PF interfaces Parametrize every RxTxApp tx pacing_way (auto, rl, tsc, tsc_narrow, ptp, be, tsn) against both VF and PF interfaces. VF-tsn is skipped: tsn launch-time pacing has no HW PHC to compare against on a VF and is rejected at mtl_init() by dev_if_init_pacing(), so it has no positive case here. Signed-off-by: Kasiewicz, Marek --- .../tests/single/st20p/test_pacing_way.py | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 tests/validation/tests/single/st20p/test_pacing_way.py diff --git a/tests/validation/tests/single/st20p/test_pacing_way.py b/tests/validation/tests/single/st20p/test_pacing_way.py new file mode 100644 index 000000000..d1fe4a6bc --- /dev/null +++ b/tests/validation/tests/single/st20p/test_pacing_way.py @@ -0,0 +1,131 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright(c) 2026 Intel Corporation +"""Verify every RxTxApp TX pacing_way (auto, rl, tsc, tsc_narrow, ptp, be, tsn) +works with the TX side on both a VF and a PF interface. "PF" cases bind only +TX to the PF (pacing_way is TX-only) and keep RX on a VF, so a PF is always +left in kernel mode for netsniff-ng capture. +""" + +import pytest +from common.nicctl import InterfaceSetup +from mtl_engine import ip_pools +from mtl_engine.media_files import yuv_files_422rfc10 + +pytestmark = pytest.mark.verified + +# E810 lacks the TxPP launch-time HW engine and never advertises +# RTE_ETH_TX_OFFLOAD_SEND_ON_TIMESTAMP, so tsn pacing is rejected at +# mtl_init() time on E810 even on a PF (see doc/design.md#4.3.3). +E810_DEVICE_ID = "1592" + + +def _is_e810(host) -> bool: + """True if the host's first NIC is an Intel E810 Series Ethernet Adapter.""" + device_id = str(host.network_interfaces[0].pci_device.device_id).lower() + return device_id == E810_DEVICE_ID + + +# tsn launch-time pacing compares the packet timestamp against the NIC's +# hardware PHC, which is only exposed on a PF (see doc/design.md#4.3.3); a VF +# port is rejected at mtl_init() time (lib/src/dev/mt_dev.c +# dev_if_init_pacing()), so there is no positive VF-tsn case to run. +# "PF" here means the TX interface only (see module docstring); RX always +# uses a VF. +PACING_WAY_CASES = [ + pytest.param("VF", "auto", id="VF-auto"), + pytest.param("PF", "auto", id="PF-auto"), + pytest.param("VF", "rl", id="VF-rl"), + pytest.param("PF", "rl", id="PF-rl"), + pytest.param("VF", "tsc", id="VF-tsc"), + pytest.param("PF", "tsc", id="PF-tsc"), + pytest.param("VF", "tsc_narrow", id="VF-tsc_narrow"), + pytest.param("PF", "tsc_narrow", id="PF-tsc_narrow"), + pytest.param("VF", "ptp", id="VF-ptp"), + pytest.param("PF", "ptp", id="PF-ptp"), + pytest.param("VF", "be", id="VF-be"), + pytest.param("PF", "be", id="PF-be"), + pytest.param( + "VF", + "tsn", + marks=pytest.mark.skip( + reason="tsn launch-time pacing requires a PF; VF has no HW PHC" + ), + id="VF-tsn", + ), + pytest.param("PF", "tsn", id="PF-tsn"), +] + + +@pytest.mark.nightly +@pytest.mark.parametrize("interface_type,pacing_way", PACING_WAY_CASES) +@pytest.mark.parametrize( + "media_file", + [yuv_files_422rfc10["ParkJoy_1080p"]], + indirect=["media_file"], + ids=["ParkJoy_1080p"], +) +def test_st20p_pacing_way( + app_factory, + hosts, + mtl_path, + setup_interfaces: InterfaceSetup, + test_time, + interface_type, + pacing_way, + pcap_capture, + media_file, +): + """Test RxTxApp TX pacing_way on a VF or PF interface.""" + media_file_info, media_file_path = media_file + host = list(hosts.values())[0] + if interface_type == "PF": + # pacing_way only affects the TX side, so only TX needs to bind to + # the PF; RX stays on a VF, leaving the other PF free (kernel-mode) + # for netsniff-ng to capture on. + interfaces_list = setup_interfaces.get_mixed_interfaces_list_single( + tx_interface_type="PF", rx_interface_type="VF" + ) + else: + interfaces_list = setup_interfaces.get_interfaces_list_single(interface_type) + + # ptp and tsn pace against the ptp-synced phc; give PTP convergence + # (handled internally via enable_ptp) more headroom than the 30s default. + needs_ptp = pacing_way in ("ptp", "tsn") + actual_test_time = max(test_time, 60) if needs_ptp else test_time + + config_params = { + "session_type": "st20p", + "nic_port_list": interfaces_list, + "source_ip": ip_pools.tx[0], + "destination_ip": ip_pools.rx[0], + "port": 20000, + "width": media_file_info["width"], + "height": media_file_info["height"], + "framerate": f"p{media_file_info['fps']}", + "pixel_format": media_file_info["file_format"], + "transport_format": media_file_info["format"], + "input_file": media_file_path, + "test_mode": "unicast", + "pacing_way": pacing_way, + "enable_ptp": needs_ptp, + "test_time": actual_test_time, + } + + app = app_factory("rxtxapp") + app.create_command(**config_params) + + if pacing_way == "tsn" and _is_e810(host): + # No TxPP launch-time HW on E810: expect mtl_init() to reject tsn + # pacing and the app to fail to start, rather than pass. + assert not app.execute_test( + build=mtl_path, + test_time=actual_test_time, + host=host, + netsniff=pcap_capture, + fail_on_error=False, + ), "tsn pacing unexpectedly succeeded on E810 (no TxPP launch-time HW)" + return + + app.execute_test( + build=mtl_path, test_time=actual_test_time, host=host, netsniff=pcap_capture + ) From 7a483bb7b01f312f2c52717d7751d3baca3c3959 Mon Sep 17 00:00:00 2001 From: "Kasiewicz, Marek" Date: Mon, 13 Jul 2026 10:44:55 +0000 Subject: [PATCH 11/19] Refactor: Move Intel NIC device ID constants to const.py E810_DEVICE_ID lived inline in test_pacing_way.py even though NIC device IDs are cross-test facts, not test-specific state (dma.py follows the same shared-constant pattern in mtl_engine). Move it to const.py alongside new E830/E835 IDs so other tests can reuse them. Signed-off-by: Kasiewicz, Marek --- tests/validation/mtl_engine/const.py | 6 ++++++ .../tests/single/st20p/test_pacing_way.py | 13 +++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/validation/mtl_engine/const.py b/tests/validation/mtl_engine/const.py index c800e46a3..4ce55f216 100644 --- a/tests/validation/mtl_engine/const.py +++ b/tests/validation/mtl_engine/const.py @@ -5,6 +5,12 @@ PERF_LOG_FOLDER = f"{LOG_FOLDER}/performance" TESTCMD_LVL = 24 # Custom logging level for test commands FRAMES_CAPTURE = 4 # Number of frames to capture for compliance tests + +# Intel NIC PCI device IDs (lowercased, vendor-less form of pci_device.device_id) +E810_DEVICE_ID = "1592" # Intel E810 Series Ethernet Adapter +E830_DEVICE_ID = "12d2" # Intel E830 Series Ethernet Adapter +E835_DEVICE_ID = "1249" # Intel E835 Series Ethernet Adapter + PREFIX = ".local_install" FFMPEG_PATH = f"{PREFIX}/ffmpeg/bin" FFMPEG_EXE = f"{FFMPEG_PATH}/ffmpeg" diff --git a/tests/validation/tests/single/st20p/test_pacing_way.py b/tests/validation/tests/single/st20p/test_pacing_way.py index d1fe4a6bc..27198e2a6 100644 --- a/tests/validation/tests/single/st20p/test_pacing_way.py +++ b/tests/validation/tests/single/st20p/test_pacing_way.py @@ -9,18 +9,19 @@ import pytest from common.nicctl import InterfaceSetup from mtl_engine import ip_pools +from mtl_engine.const import E810_DEVICE_ID from mtl_engine.media_files import yuv_files_422rfc10 pytestmark = pytest.mark.verified -# E810 lacks the TxPP launch-time HW engine and never advertises -# RTE_ETH_TX_OFFLOAD_SEND_ON_TIMESTAMP, so tsn pacing is rejected at -# mtl_init() time on E810 even on a PF (see doc/design.md#4.3.3). -E810_DEVICE_ID = "1592" - def _is_e810(host) -> bool: - """True if the host's first NIC is an Intel E810 Series Ethernet Adapter.""" + """True if the host's first NIC is an Intel E810 Series Ethernet Adapter. + + E810 lacks the TxPP launch-time HW engine and never advertises + RTE_ETH_TX_OFFLOAD_SEND_ON_TIMESTAMP, so tsn pacing is rejected at + mtl_init() time on E810 even on a PF (see doc/design.md#4.3.3). + """ device_id = str(host.network_interfaces[0].pci_device.device_id).lower() return device_id == E810_DEVICE_ID From f7e944213e28cd9956e82d006162ae58216a94d3 Mon Sep 17 00:00:00 2001 From: "Kasiewicz, Marek" Date: Mon, 13 Jul 2026 10:45:06 +0000 Subject: [PATCH 12/19] Test: Parametrize st20p pacing_way test over application RxTxApp is currently the only backend that takes pacing_way, so add the same application axis test_interlace.py/test_pacing.py use, with ffmpeg skipped, instead of hardcoding "rxtxapp" in the test body. Signed-off-by: Kasiewicz, Marek --- .../tests/single/st20p/test_pacing_way.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/validation/tests/single/st20p/test_pacing_way.py b/tests/validation/tests/single/st20p/test_pacing_way.py index 27198e2a6..d57b5ccd0 100644 --- a/tests/validation/tests/single/st20p/test_pacing_way.py +++ b/tests/validation/tests/single/st20p/test_pacing_way.py @@ -58,6 +58,18 @@ def _is_e810(host) -> bool: @pytest.mark.nightly +@pytest.mark.parametrize( + "application", + [ + "rxtxapp", + pytest.param( + "ffmpeg", + marks=pytest.mark.skip( + reason="FFmpeg does not support pacing_way selection" + ), + ), + ], +) @pytest.mark.parametrize("interface_type,pacing_way", PACING_WAY_CASES) @pytest.mark.parametrize( "media_file", @@ -66,6 +78,7 @@ def _is_e810(host) -> bool: ids=["ParkJoy_1080p"], ) def test_st20p_pacing_way( + application, app_factory, hosts, mtl_path, @@ -112,7 +125,7 @@ def test_st20p_pacing_way( "test_time": actual_test_time, } - app = app_factory("rxtxapp") + app = app_factory(application) app.create_command(**config_params) if pacing_way == "tsn" and _is_e810(host): From d3754212df54d408fd0e059591e17803e9b9878e Mon Sep 17 00:00:00 2001 From: "Kasiewicz, Marek" Date: Mon, 13 Jul 2026 13:43:07 +0000 Subject: [PATCH 13/19] Fix: Honor application TX pacing timestamps Keep ST20/ST40 user timing authoritative across epoch recovery. Validate exact targets, unwrap ST40 media clocks, and preserve wait boundaries and completion metadata. Add deterministic pacing/stat tests. Fixes: dad214ffa5a3 ("Fix: Prevent stuck epoch recovery on ST20/ST40 TX pacing") Signed-off-by: Kasiewicz, Marek --- lib/src/st2110/st_fmt.c | 6 +- lib/src/st2110/st_fmt.h | 2 + lib/src/st2110/st_tx_ancillary_session.c | 104 ++- lib/src/st2110/st_tx_video_session.c | 81 ++- lib/src/st2110/st_video_transmitter.c | 12 +- tests/unit/meson.build | 2 + tests/unit/session/st20_tx/epoch_test.cpp | 246 ++++--- tests/unit/session/st20_tx/pacing_test.cpp | 533 ++++++++++++++- tests/unit/session/st20_tx_harness.c | 245 ++++++- tests/unit/session/st20_tx_harness.h | 27 +- tests/unit/session/st40_tx/pacing_test.cpp | 758 +++++++++++++++++++++ tests/unit/session/st40_tx_harness.c | 340 +++++++++ tests/unit/session/st40_tx_harness.h | 69 ++ 13 files changed, 2233 insertions(+), 192 deletions(-) create mode 100644 tests/unit/session/st40_tx/pacing_test.cpp create mode 100644 tests/unit/session/st40_tx_harness.c create mode 100644 tests/unit/session/st40_tx_harness.h diff --git a/lib/src/st2110/st_fmt.c b/lib/src/st2110/st_fmt.c index eb7a228b4..2a8d7b245 100644 --- a/lib/src/st2110/st_fmt.c +++ b/lib/src/st2110/st_fmt.c @@ -928,7 +928,7 @@ uint32_t st10_tai_to_media_clk(uint64_t tai_ns, uint32_t sampling_rate) { return (uint32_t)st_muldiv_u64_round_closest(tai_ns, sampling_rate, NS_PER_S); } -uint64_t st10_media_clk_to_ns(uint32_t media_ts, uint32_t sampling_rate) { +uint64_t st10_media_clk_to_ns_u64(uint64_t media_ts, uint32_t sampling_rate) { if (!sampling_rate) { err("%s, invalid sampling rate\n", __func__); return 0; @@ -937,6 +937,10 @@ uint64_t st10_media_clk_to_ns(uint32_t media_ts, uint32_t sampling_rate) { return st_muldiv_u64_round_closest(media_ts, NS_PER_S, sampling_rate); } +uint64_t st10_media_clk_to_ns(uint32_t media_ts, uint32_t sampling_rate) { + return st10_media_clk_to_ns_u64(media_ts, sampling_rate); +} + int st_draw_logo(struct st_frame* frame, struct st_frame* logo, uint32_t x, uint32_t y) { if (frame->fmt != logo->fmt) { err("%s, mismatch fmt %d %d\n", __func__, frame->fmt, logo->fmt); diff --git a/lib/src/st2110/st_fmt.h b/lib/src/st2110/st_fmt.h index 2f846a355..b1878ead3 100644 --- a/lib/src/st2110/st_fmt.h +++ b/lib/src/st2110/st_fmt.h @@ -39,6 +39,8 @@ const char* st_tx_pacing_way_name(enum st21_tx_pacing_way way); int st_get_fps_timing(enum st_fps fps, struct st_fps_timing* fps_tm); +uint64_t st10_media_clk_to_ns_u64(uint64_t media_ts, uint32_t sampling_rate); + enum st_frame_sampling st_frame_fmt_get_sampling(enum st_frame_fmt fmt); int st22_rtp_bandwidth_bps(uint32_t total_pkts, uint16_t pkt_size, enum st_fps fps, diff --git a/lib/src/st2110/st_tx_ancillary_session.c b/lib/src/st2110/st_tx_ancillary_session.c index d2828ad28..b67245fee 100644 --- a/lib/src/st2110/st_tx_ancillary_session.c +++ b/lib/src/st2110/st_tx_ancillary_session.c @@ -283,7 +283,9 @@ static int tx_ancillary_session_init_pacing_epoch( static inline uint64_t tx_ancillary_pacing_time( struct st_tx_ancillary_session_pacing* pacing, uint64_t epochs) { - return nextafterl((long double)epochs * pacing->frame_time, INFINITY); + long double tai = nextafterl((long double)epochs * pacing->frame_time, INFINITY); + if (tai >= UINT64_MAX) return UINT64_MAX; + return tai; } static inline __attribute__((unused)) uint32_t tx_ancillary_pacing_time_stamp( @@ -298,9 +300,10 @@ static uint64_t tx_ancillary_pacing_required_tai(struct st_tx_ancillary_session_ enum st10_timestamp_fmt tfmt, uint64_t timestamp) { uint64_t required_tai = 0; + uint64_t cur_tai; if (!(s->ops.flags & ST40_TX_FLAG_USER_PACING)) return 0; - if (!timestamp) { + if (!timestamp && tfmt != ST10_TIMESTAMP_FMT_MEDIA_CLK) { if (s->ops.flags & ST40_TX_FLAG_EXACT_USER_PACING) { s->port_user_stats.common.stat_error_user_timestamp++; err("%s(%d), EXACT_USER_PACING requires non-zero timestamp\n", __func__, s->idx); @@ -309,29 +312,57 @@ static uint64_t tx_ancillary_pacing_required_tai(struct st_tx_ancillary_session_ } if (tfmt == ST10_TIMESTAMP_FMT_MEDIA_CLK) { - if (timestamp > 0xFFFFFFFF) { + uint64_t current_ticks; + uint64_t target_ticks; + uint32_t forward; + + if (timestamp > UINT32_MAX) { err("%s(%d), invalid timestamp %" PRIu64 "\n", __func__, s->idx, timestamp); + s->port_user_stats.common.stat_error_user_timestamp++; + return 0; + } + cur_tai = mt_get_ptp_time(s->mgr->parent, MTL_PORT_P); + current_ticks = (cur_tai / NS_PER_S) * ST10_VIDEO_SAMPLING_RATE_90K; + current_ticks += + st10_tai_to_media_clk(cur_tai % NS_PER_S, ST10_VIDEO_SAMPLING_RATE_90K); + + forward = (uint32_t)timestamp - (uint32_t)current_ticks; + if (forward <= INT32_MAX) { + target_ticks = current_ticks + forward; + } else { + uint64_t backward = (uint64_t)UINT32_MAX - forward + 1; + target_ticks = (current_ticks >= backward) ? current_ticks - backward + : current_ticks + forward; } - required_tai = st10_media_clk_to_ns((uint32_t)timestamp, 90 * 1000); + + required_tai = st10_media_clk_to_ns_u64(target_ticks, ST10_VIDEO_SAMPLING_RATE_90K); } else { required_tai = timestamp; + cur_tai = mt_get_ptp_time(s->mgr->parent, MTL_PORT_P); + } + + if ((s->ops.flags & ST40_TX_FLAG_EXACT_USER_PACING) && + (required_tai < cur_tai || required_tai - cur_tai > NS_PER_S)) { + s->port_user_stats.common.stat_error_user_timestamp++; + return 0; } return required_tai; } static void tx_ancillary_validate_user_timestamp(struct st_tx_ancillary_session_impl* s, - uint64_t requested_epoch, - uint64_t current_epoch) { - if (requested_epoch < current_epoch) { + uint64_t requested_time, + uint64_t current_time, + uint64_t max_future) { + if (requested_time < current_time) { s->port_user_stats.common.stat_error_user_timestamp++; - dbg("%s(%d), user requested transmission time in the past, required_epoch %" PRIu64 - ", cur_epoch %" PRIu64 "\n", - __func__, s->idx, requested_epoch, current_epoch); - } else if (requested_epoch > current_epoch + (NS_PER_S / s->pacing.frame_time)) { - dbg("%s(%d), requested epoch %" PRIu64 - " too far in the future, current epoch %" PRIu64 "\n", - __func__, s->idx, requested_epoch, current_epoch); + dbg("%s(%d), user requested transmission time in the past, requested %" PRIu64 + ", current %" PRIu64 "\n", + __func__, s->idx, requested_time, current_time); + } else if (requested_time - current_time > max_future) { + dbg("%s(%d), requested time %" PRIu64 " too far in the future, current time %" PRIu64 + "\n", + __func__, s->idx, requested_time, current_time); s->port_user_stats.common.stat_error_user_timestamp++; } } @@ -340,17 +371,27 @@ static inline uint64_t tx_ancillary_calc_epoch(struct st_tx_ancillary_session_im uint64_t cur_tai, uint64_t required_tai) { struct st_tx_ancillary_session_pacing* pacing = &s->pacing; uint64_t current_epoch = cur_tai / pacing->frame_time; - uint64_t next_free_epoch = pacing->cur_epochs + 1; - uint64_t epoch = next_free_epoch; + uint64_t next_free_epoch; + uint64_t epoch; if (required_tai) { epoch = (required_tai + pacing->frame_time / 2) / pacing->frame_time; - tx_ancillary_validate_user_timestamp(s, epoch, current_epoch); + if (s->ops.flags & ST40_TX_FLAG_EXACT_USER_PACING) { + tx_ancillary_validate_user_timestamp(s, required_tai, cur_tai, NS_PER_S); + } else { + tx_ancillary_validate_user_timestamp(s, epoch, current_epoch, + NS_PER_S / pacing->frame_time); + } /* epoch 0 collides with the "unset" sentinel used elsewhere; fall back to * real time rather than honor a rounded-to-zero required_tai */ if (!epoch) epoch = current_epoch; + return epoch; } + if (pacing->cur_epochs == UINT64_MAX) return current_epoch; + next_free_epoch = pacing->cur_epochs + 1; + epoch = next_free_epoch; + if (current_epoch <= next_free_epoch) { uint64_t onward = next_free_epoch - current_epoch; if (onward > pacing->max_onward_epochs) { @@ -360,8 +401,8 @@ static inline uint64_t tx_ancillary_calc_epoch(struct st_tx_ancillary_session_im ", current_epoch %" PRIu64 "\n", __func__, s->idx, next_free_epoch, current_epoch); s->port_user_stats.common.stat_epoch_onward += onward; - if (!required_tai) epoch = current_epoch; - } else if (!required_tai) { + epoch = current_epoch; + } else { epoch = next_free_epoch; } } else { @@ -373,7 +414,7 @@ static inline uint64_t tx_ancillary_calc_epoch(struct st_tx_ancillary_session_im s->ops.notify_frame_late(s->ops.priv, current_epoch - next_free_epoch); } - if (!required_tai) epoch = current_epoch; + epoch = current_epoch; } return epoch; @@ -386,7 +427,7 @@ static int tx_ancillary_session_sync_pacing(struct mtl_main_impl* impl, uint64_t cur_tai = mt_get_ptp_time(impl, MTL_PORT_P); uint64_t cur_tsc = mt_get_tsc(impl); uint64_t start_time_tai; - int64_t time_to_tx_ns; + uint64_t time_to_tx_ns; pacing->cur_epochs = tx_ancillary_calc_epoch(s, cur_tai, required_tai); @@ -395,16 +436,19 @@ static int tx_ancillary_session_sync_pacing(struct mtl_main_impl* impl, } else { start_time_tai = tx_ancillary_pacing_time(pacing, pacing->cur_epochs); } - time_to_tx_ns = (int64_t)start_time_tai - (int64_t)cur_tai; - if (time_to_tx_ns < 0) { - /* time bigger than the assigned epoch time */ + if (start_time_tai < cur_tai) { s->port_user_stats.common.stat_epoch_mismatch++; - time_to_tx_ns = 0; /* send asap */ + time_to_tx_ns = 0; + } else { + time_to_tx_ns = start_time_tai - cur_tai; } pacing->ptp_time_cursor = start_time_tai; - pacing->tsc_time_cursor = (long double)cur_tsc + (long double)time_to_tx_ns; - dbg("%s(%d), epochs %" PRIu64 " ptp_time_cursor %Lf time_to_tx_ns %" PRId64 "\n", + if (time_to_tx_ns > UINT64_MAX - cur_tsc) + pacing->tsc_time_cursor = UINT64_MAX; + else + pacing->tsc_time_cursor = cur_tsc + time_to_tx_ns; + dbg("%s(%d), epochs %" PRIu64 " ptp_time_cursor %Lf time_to_tx_ns %" PRIu64 "\n", __func__, s->idx, pacing->cur_epochs, pacing->ptp_time_cursor, time_to_tx_ns); return 0; @@ -962,6 +1006,7 @@ static int tx_ancillary_session_tasklet_frame(struct mtl_main_impl* impl, frame->tc_meta.tfmt = ST10_TIMESTAMP_FMT_TAI; frame->tc_meta.timestamp = pacing->ptp_time_cursor; frame->tc_meta.rtp_timestamp = pacing->rtp_time_stamp; + frame->tc_meta.epoch = pacing->cur_epochs; /* init to next field */ if (ops->interlaced) { s->second_field = second_field ? false : true; @@ -975,7 +1020,7 @@ static int tx_ancillary_session_tasklet_frame(struct mtl_main_impl* impl, uint64_t delta = target_tsc - cur_tsc; // dbg("%s(%d), cur_tsc %"PRIu64" target_tsc %"PRIu64"\n", __func__, idx, cur_tsc, // target_tsc); - if (likely(delta < NS_PER_S)) { + if (likely(delta <= NS_PER_S)) { s->stat_build_ret_code = -STI_TSCTRS_TARGET_TSC_NOT_REACH; return delta < mt_sch_schedule_ns(impl) ? MTL_TASKLET_HAS_PENDING : MTL_TASKLET_ALL_DONE; @@ -1063,6 +1108,7 @@ static int tx_ancillary_session_tasklet_frame(struct mtl_main_impl* impl, s->st40_pkt_idx++; double pkt_time = pacing->frame_time / RTE_MAX(1, s->st40_total_pkts); TX_ANC_TEST_PACING_OVERRIDE(s, pkt_time); + /* Session uptime cannot approach the uint64_t nanosecond horizon. */ pacing->tsc_time_cursor += pkt_time; /* keep one RTP timestamp across a multi-packet frame; re-sync after the last pkt */ s->calculate_time_cursor = s->st40_pkt_idx >= s->st40_total_pkts; @@ -1166,7 +1212,7 @@ static int tx_ancillary_session_tasklet_rtp(struct mtl_main_impl* impl, uint64_t delta = target_tsc - cur_tsc; // dbg("%s(%d), cur_tsc %"PRIu64" target_tsc %"PRIu64"\n", __func__, idx, cur_tsc, // target_tsc); - if (likely(delta < NS_PER_S)) { + if (likely(delta <= NS_PER_S)) { s->stat_build_ret_code = -STI_TSCTRS_TARGET_TSC_NOT_REACH; return delta < mt_sch_schedule_ns(impl) ? MTL_TASKLET_HAS_PENDING : MTL_TASKLET_ALL_DONE; diff --git a/lib/src/st2110/st_tx_video_session.c b/lib/src/st2110/st_tx_video_session.c index d245b713e..9d77f0b37 100644 --- a/lib/src/st2110/st_tx_video_session.c +++ b/lib/src/st2110/st_tx_video_session.c @@ -57,14 +57,19 @@ static inline uint64_t tai_from_frame_count(struct st_tx_video_pacing* pacing, * may truncate to a smaller value. Using nextafter(val, INFINITY) ensures we round * up to the next representable double before casting, avoiding jumping between tai in * neighboring frames. This caused problems when tai was again changed to frame count */ - return nextafterl((long double)frame_count * pacing->frame_time, INFINITY); + long double tai = nextafterl((long double)frame_count * pacing->frame_time, INFINITY); + if (tai >= UINT64_MAX) return UINT64_MAX; + return tai; } /* transmission start time of the frame */ static inline uint64_t transmission_start_time(struct st_tx_video_pacing* pacing, uint64_t frame_count) { - return tai_from_frame_count(pacing, frame_count) + pacing->tr_offset - - (pacing->vrx * pacing->trs); + long double start_time = (long double)tai_from_frame_count(pacing, frame_count) + + pacing->tr_offset - (pacing->vrx * pacing->trs); + if (start_time <= 0) return 0; + if (start_time >= UINT64_MAX) return UINT64_MAX; + return start_time; } static inline void pacing_set_mbuf_time_stamp(struct rte_mbuf* mbuf, @@ -629,18 +634,17 @@ static int tv_init_pacing_epoch(struct mtl_main_impl* impl, } static void validate_user_timestamp(struct st_tx_video_session_impl* s, - uint64_t requested_frame_count, - uint64_t current_frame_count) { - if (requested_frame_count < current_frame_count) { + uint64_t requested_time, uint64_t current_time, + uint64_t max_future) { + if (requested_time < current_time) { s->port_user_stats.common.stat_error_user_timestamp++; dbg("%s(%d), user requested transmission time in the past, required_tai %" PRIu64 ", cur_tai %" PRIu64 "\n", - __func__, s->idx, requested_frame_count, current_frame_count); - } else if (requested_frame_count > - current_frame_count + (NS_PER_S / s->pacing.frame_time)) { - dbg("%s(%d), requested frame count %" PRIu64 - " too far in the future, current frame count %" PRIu64 "\n", - __func__, s->idx, requested_frame_count, current_frame_count); + __func__, s->idx, requested_time, current_time); + } else if (requested_time - current_time > max_future) { + dbg("%s(%d), requested time %" PRIu64 " too far in the future, current time %" PRIu64 + "\n", + __func__, s->idx, requested_time, current_time); s->port_user_stats.common.stat_error_user_timestamp++; } } @@ -649,17 +653,26 @@ static inline uint64_t calc_frame_count_since_epoch(struct st_tx_video_session_i uint64_t cur_tai, uint64_t required_tai) { uint64_t frame_count_tai = cur_tai / s->pacing.frame_time; - uint64_t next_free_frame_slot = s->pacing.cur_epochs + 1; + uint64_t next_free_frame_slot; uint64_t frame_count; if (required_tai) { frame_count = (required_tai + s->pacing.frame_time / 2) / s->pacing.frame_time; - validate_user_timestamp(s, frame_count, frame_count_tai); + if (s->ops.flags & ST20_TX_FLAG_EXACT_USER_PACING) { + validate_user_timestamp(s, required_tai, cur_tai, NS_PER_S); + } else { + validate_user_timestamp(s, frame_count, frame_count_tai, + NS_PER_S / s->pacing.frame_time); + } /* frame_count 0 collides with the "unset" sentinel used elsewhere; fall back to * real time rather than honor a rounded-to-zero required_tai */ if (!frame_count) frame_count = frame_count_tai; + return frame_count; } + if (s->pacing.cur_epochs == UINT64_MAX) return frame_count_tai; + next_free_frame_slot = s->pacing.cur_epochs + 1; + if (frame_count_tai <= next_free_frame_slot) { /* There is time buffer until the next available frame time window */ uint64_t onward = next_free_frame_slot - frame_count_tai; @@ -670,8 +683,8 @@ static inline uint64_t calc_frame_count_since_epoch(struct st_tx_video_session_i ", frame_count_tai %" PRIu64 "\n", __func__, s->idx, next_free_frame_slot, frame_count_tai); s->port_user_stats.common.stat_epoch_onward += onward; - if (!required_tai) frame_count = frame_count_tai; - } else if (!required_tai) { + frame_count = frame_count_tai; + } else { frame_count = next_free_frame_slot; } @@ -685,7 +698,7 @@ static inline uint64_t calc_frame_count_since_epoch(struct st_tx_video_session_i s->ops.notify_frame_late(s->ops.priv, frame_count_tai - next_free_frame_slot); } - if (!required_tai) frame_count = frame_count_tai; + frame_count = frame_count_tai; } return frame_count; @@ -697,28 +710,27 @@ static int tv_sync_pacing(struct mtl_main_impl* impl, struct st_tx_video_session uint64_t cur_tai = mt_get_ptp_time(impl, MTL_PORT_P); uint64_t cur_tsc = mt_get_tsc(impl); uint64_t start_time_tai; - int64_t time_to_tx_ns; + uint64_t time_to_tx_ns; pacing->cur_epochs = calc_frame_count_since_epoch(s, cur_tai, required_tai); - if (s->ops.flags & ST20_TX_FLAG_EXACT_USER_PACING) { + if ((s->ops.flags & ST20_TX_FLAG_EXACT_USER_PACING) && required_tai) { start_time_tai = required_tai; } else { start_time_tai = transmission_start_time(pacing, pacing->cur_epochs); } - time_to_tx_ns = start_time_tai - cur_tai; - - if (time_to_tx_ns < 0) { - /* should never happen, but it does. TODO: check why */ - dbg("%s(%d), negative time_to_tx_ns detected: %ld ns. Current PTP time: %" PRIu64 - "\n", - __func__, s->idx, time_to_tx_ns, cur_tai); + if (start_time_tai < cur_tai) { time_to_tx_ns = 0; + } else { + time_to_tx_ns = start_time_tai - cur_tai; } /* tsc_time_cursor is important as it determines when first packet of the frame will be * send */ - pacing->tsc_time_cursor = cur_tsc + time_to_tx_ns; + if (time_to_tx_ns > UINT64_MAX - cur_tsc) + pacing->tsc_time_cursor = UINT64_MAX; + else + pacing->tsc_time_cursor = cur_tsc + time_to_tx_ns; pacing->tsc_time_frame_start = pacing->tsc_time_cursor; pacing->ptp_time_cursor = start_time_tai; @@ -732,7 +744,7 @@ static int tv_sync_pacing_st22(struct mtl_main_impl* impl, struct st_tx_video_pacing* pacing = &s->pacing; /* reset trs */ pacing->trs = pacing->frame_time * pacing->reactive / pkts_in_frame; - dbg("%s(%d), trs %f\n", __func__, s->idx, pacing->trs); + dbg("%s(%d), trs %Lf\n", __func__, s->idx, pacing->trs); return tv_sync_pacing(impl, s, required_tai); } @@ -1742,6 +1754,7 @@ static uint64_t tv_pacing_required_tai(struct st_tx_video_session_impl* s, } if (tfmt == ST10_TIMESTAMP_FMT_MEDIA_CLK) { + s->port_user_stats.common.stat_error_user_timestamp++; err("%s(%d), Media clock can't be used for user-controlled pacing\n", __func__, s->idx); return 0; // Return 0 to indicate an invalid timestamp and fallback to default pacing @@ -1749,6 +1762,14 @@ static uint64_t tv_pacing_required_tai(struct st_tx_video_session_impl* s, required_tai = timestamp; } + if (s->ops.flags & ST20_TX_FLAG_EXACT_USER_PACING) { + uint64_t cur_tai = mt_get_ptp_time(s->impl, MTL_PORT_P); + if (required_tai < cur_tai || required_tai - cur_tai > NS_PER_S) { + s->port_user_stats.common.stat_error_user_timestamp++; + return 0; + } + } + return required_tai; } @@ -2080,7 +2101,7 @@ static int tv_tasklet_frame(struct mtl_main_impl* impl, if (frame_end_time > pacing->tsc_time_cursor) { s->port_user_stats.common.stat_exceed_frame_time++; rte_atomic32_inc(&s->cbs_build_timeout); - dbg("%s(%d), frame %d build time out %ldus\n", __func__, idx, s->st20_frame_idx, + dbg("%s(%d), frame %d build time out %Lfus\n", __func__, idx, s->st20_frame_idx, (frame_end_time - pacing->tsc_time_cursor) / NS_PER_US); } } @@ -2584,7 +2605,7 @@ static int tv_tasklet_st22(struct mtl_main_impl* impl, if (frame_end_time > pacing->tsc_time_cursor) { s->port_user_stats.common.stat_exceed_frame_time++; rte_atomic32_inc(&s->cbs_build_timeout); - dbg("%s(%d), frame %d build time out %ldus\n", __func__, idx, s->st20_frame_idx, + dbg("%s(%d), frame %d build time out %Lfus\n", __func__, idx, s->st20_frame_idx, (frame_end_time - pacing->tsc_time_cursor) / NS_PER_US); } } diff --git a/lib/src/st2110/st_video_transmitter.c b/lib/src/st2110/st_video_transmitter.c index c8669f5fc..d429d6bb2 100644 --- a/lib/src/st2110/st_video_transmitter.c +++ b/lib/src/st2110/st_video_transmitter.c @@ -200,7 +200,7 @@ static int _video_trs_rl_tasklet(struct mtl_main_impl* impl, uint64_t cur_tsc = mt_get_tsc(impl); if (cur_tsc < target_tsc) { uint64_t delta = target_tsc - cur_tsc; - if (likely(delta < NS_PER_S)) { + if (likely(delta <= NS_PER_S)) { *ret_status = -STI_RLTRS_TARGET_TSC_NOT_REACH; return delta < mt_sch_schedule_ns(impl) ? MTL_TASKLET_HAS_PENDING : MTL_TASKLET_ALL_DONE; @@ -287,7 +287,7 @@ static int _video_trs_rl_tasklet(struct mtl_main_impl* impl, unsigned int i; uint64_t delta = target_tsc - cur_tsc; - if (likely(delta < NS_PER_S || s->trs_inflight_num2[s_port])) { + if (likely(delta <= NS_PER_S || s->trs_inflight_num2[s_port])) { s->trs_target_tsc[s_port] = target_tsc; s->trs_target_ptp[s_port] = target_ptp; /* save it on inflight */ @@ -353,7 +353,7 @@ static int video_trs_tsc_tasklet(struct mtl_main_impl* impl, cur_tsc = mt_get_tsc(impl); if (cur_tsc < target_tsc) { uint64_t delta = target_tsc - cur_tsc; - if (likely(delta < NS_PER_S)) { + if (likely(delta <= NS_PER_S)) { s->stat_trs_ret_code[s_port] = -STI_TSCTRS_TARGET_TSC_NOT_REACH; return delta < mt_sch_schedule_ns(impl) ? MTL_TASKLET_HAS_PENDING : MTL_TASKLET_ALL_DONE; @@ -412,7 +412,7 @@ static int video_trs_tsc_tasklet(struct mtl_main_impl* impl, unsigned int i; uint64_t delta = target_tsc - cur_tsc; - if (likely(delta < NS_PER_S)) { + if (likely(delta <= NS_PER_S)) { s->trs_target_tsc[s_port] = target_tsc; /* save it on inflight */ s->trs_inflight_num[s_port] = valid_bulk; @@ -540,7 +540,7 @@ static int video_trs_ptp_tasklet(struct mtl_main_impl* impl, cur_ptp = mt_get_ptp_time(impl, MTL_PORT_P); if (cur_ptp < target_ptp) { uint64_t delta = target_ptp - cur_ptp; - if (likely(delta < NS_PER_S)) { + if (likely(delta <= NS_PER_S)) { s->stat_trs_ret_code[s_port] = -STI_TSCTRS_TARGET_TSC_NOT_REACH; return delta < mt_sch_schedule_ns(impl) ? MTL_TASKLET_HAS_PENDING : MTL_TASKLET_ALL_DONE; @@ -598,7 +598,7 @@ static int video_trs_ptp_tasklet(struct mtl_main_impl* impl, unsigned int i; uint64_t delta = target_ptp - cur_ptp; - if (likely(delta < NS_PER_S)) { + if (likely(delta <= NS_PER_S)) { s->trs_target_tsc[s_port] = target_ptp; /* save it on inflight */ s->trs_inflight_num[s_port] = valid_bulk; diff --git a/tests/unit/meson.build b/tests/unit/meson.build index bf45276d6..de86d4dca 100644 --- a/tests/unit/meson.build +++ b/tests/unit/meson.build @@ -41,6 +41,8 @@ unit_sources = [ 'session/st40/rfc8331_payload_bytes_test.cpp', 'session/st40/rfc8331_codec_test.cpp', 'session/st40/tx_corrupt_parity_test.cpp', + 'session/st40_tx_harness.c', + 'session/st40_tx/pacing_test.cpp', 'session/st30_harness.c', 'session/st30/redundancy_test.cpp', 'session/st30/timestamp_test.cpp', diff --git a/tests/unit/session/st20_tx/epoch_test.cpp b/tests/unit/session/st20_tx/epoch_test.cpp index 647b6d23f..ed4c5c073 100644 --- a/tests/unit/session/st20_tx/epoch_test.cpp +++ b/tests/unit/session/st20_tx/epoch_test.cpp @@ -16,161 +16,243 @@ #include "session/st20_tx_harness.h" +namespace { +constexpr uint64_t kNanosecondsPerMillisecond = 1000 * 1000; +constexpr uint64_t kNanosecondsPerSecond = 1000 * kNanosecondsPerMillisecond; +constexpr uint64_t kFramePeriodNs = kNanosecondsPerMillisecond; +constexpr uint64_t kCurrentTai = 10 * kFramePeriodNs; +constexpr uint64_t kCurrentEpoch = kCurrentTai / kFramePeriodNs; +constexpr uint64_t kInitialEpoch = kCurrentEpoch - 1; +constexpr uint64_t kStaleEpoch = 1000; +constexpr uint64_t kPastTai = 5 * kFramePeriodNs; +constexpr uint64_t kOnwardRecoveryTai = 6 * kFramePeriodNs; +constexpr uint64_t kSecondOnwardRecoveryTai = 8 * kFramePeriodNs; +constexpr uint64_t kLateTai = 13 * kFramePeriodNs; +constexpr uint64_t kHalfFrameTargetTai = kCurrentTai + kFramePeriodNs / 2; +constexpr uint64_t kFutureBoundaryTai = kCurrentTai + kNanosecondsPerSecond; +constexpr uint64_t kBeyondFutureBoundaryTai = kFutureBoundaryTai + kFramePeriodNs; +constexpr uint64_t kLateTargetTai = 100 * kFramePeriodNs; +} // namespace + class St20TxEpochTest : public ::testing::Test { protected: void SetUp() override { ASSERT_EQ(ut_txv_init(), 0); ctx_ = ut_txv_create(); ASSERT_NE(ctx_, nullptr); - ut_txv_set_frame_time(ctx_, 1000000.0L); + ut_txv_set_frame_time(ctx_, kFramePeriodNs); ut_txv_set_max_onward_epochs(ctx_, 3); } void TearDown() override { ut_txv_destroy(ctx_); } + void ExpectNoStatsOrLateCallback() { + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_mismatch(ctx_), 0u); + EXPECT_EQ(ut_txv_notify_late_calls(ctx_), 0); + } ut_txv_ctx* ctx_ = nullptr; }; TEST_F(St20TxEpochTest, SteadyStateAdvancesByOneEpoch) { - ut_txv_set_cur_epochs(ctx_, 9); /* next_free_frame_slot = 10 */ - uint64_t frame_count = ut_txv_calc_frame_count_since_epoch(ctx_, 10000000, 0); - EXPECT_EQ(frame_count, 10u); - EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); - EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); /* next_free_frame_slot = 10 */ + uint64_t frame_count = ut_txv_calc_frame_count_since_epoch(ctx_, kCurrentTai, 0); + EXPECT_EQ(frame_count, kCurrentEpoch); + ExpectNoStatsOrLateCallback(); } TEST_F(St20TxEpochTest, OnwardGapAtBoundaryDoesNotResync) { - ut_txv_set_cur_epochs(ctx_, 9); /* next=10 */ - uint64_t frame_count = - ut_txv_calc_frame_count_since_epoch(ctx_, 7000000, 0); /* tai=7, onward=3==max */ - EXPECT_EQ(frame_count, 10u); - EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); /* next=10 */ + uint64_t frame_count = ut_txv_calc_frame_count_since_epoch( + ctx_, 7 * kFramePeriodNs, 0); /* tai=7, onward=3==max */ + EXPECT_EQ(frame_count, kCurrentEpoch); + ExpectNoStatsOrLateCallback(); +} + +TEST_F(St20TxEpochTest, CurrentTimeAtNextSlotBoundaryDoesNotReportLate) { + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); + uint64_t frame_count = ut_txv_calc_frame_count_since_epoch(ctx_, kCurrentTai, 0); + + EXPECT_EQ(frame_count, kCurrentEpoch); + ExpectNoStatsOrLateCallback(); } TEST_F(St20TxEpochTest, OnwardGapBeyondBoundaryResyncs) { - ut_txv_set_cur_epochs(ctx_, 9); /* next=10 */ - uint64_t frame_count = - ut_txv_calc_frame_count_since_epoch(ctx_, 6000000, 0); /* tai=6, onward=4>max */ - EXPECT_EQ(frame_count, 6u); + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); /* next=10 */ + uint64_t frame_count = ut_txv_calc_frame_count_since_epoch(ctx_, kOnwardRecoveryTai, + 0); /* tai=6, onward=4>max */ + EXPECT_EQ(frame_count, kOnwardRecoveryTai / kFramePeriodNs); EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 4u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 0u); + EXPECT_EQ(ut_txv_notify_late_calls(ctx_), 0); +} + +TEST_F(St20TxEpochTest, RepeatedOnwardRecoveryAccumulatesExactGaps) { + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); + EXPECT_EQ(ut_txv_calc_frame_count_since_epoch(ctx_, kOnwardRecoveryTai, 0), + kOnwardRecoveryTai / kFramePeriodNs); + ut_txv_set_cur_epochs(ctx_, 12); + EXPECT_EQ(ut_txv_calc_frame_count_since_epoch(ctx_, kSecondOnwardRecoveryTai, 0), + kSecondOnwardRecoveryTai / kFramePeriodNs); + + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 9u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 0u); + EXPECT_EQ(ut_txv_notify_late_calls(ctx_), 0); } TEST_F(St20TxEpochTest, LargePtpStepResyncsInOneCall) { - ut_txv_set_cur_epochs(ctx_, 1000000); /* simulates a stale epoch after a PTP step */ + constexpr uint64_t stale_epoch = kStaleEpoch * kStaleEpoch; + constexpr uint64_t recovered_epoch = kPastTai / kFramePeriodNs; + constexpr uint64_t onward_gap = stale_epoch + 1 - recovered_epoch; + ut_txv_set_cur_epochs(ctx_, stale_epoch); uint64_t frame_count = - ut_txv_calc_frame_count_since_epoch(ctx_, 5000000, 0); /* tai=5 */ - EXPECT_EQ(frame_count, 5u); - EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 999996u); /* next(1000001) - tai(5) */ + ut_txv_calc_frame_count_since_epoch(ctx_, kPastTai, 0); /* tai=5 */ + EXPECT_EQ(frame_count, kPastTai / kFramePeriodNs); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), onward_gap); /* caller commits the resync (as tv_sync_pacing does) and time advances by * one frame -- confirm the gap stays closed, no further resync needed */ ut_txv_set_cur_epochs(ctx_, frame_count); - uint64_t frame_count2 = ut_txv_calc_frame_count_since_epoch(ctx_, 6000000, 0); - EXPECT_EQ(frame_count2, 6u); - EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 999996u); /* unchanged */ + uint64_t frame_count2 = + ut_txv_calc_frame_count_since_epoch(ctx_, kOnwardRecoveryTai, 0); + EXPECT_EQ(frame_count2, kOnwardRecoveryTai / kFramePeriodNs); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), onward_gap); } TEST_F(St20TxEpochTest, LatePathBumpsDropAndNotifiesFrameLate) { ut_txv_set_cur_epochs(ctx_, 2); /* next_free_frame_slot=3 */ uint64_t frame_count = - ut_txv_calc_frame_count_since_epoch(ctx_, 10000000, 0); /* tai=10 */ - EXPECT_EQ(frame_count, 10u); + ut_txv_calc_frame_count_since_epoch(ctx_, kCurrentTai, 0); /* tai=10 */ + EXPECT_EQ(frame_count, kCurrentEpoch); EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 7u); EXPECT_EQ(ut_txv_notify_late_calls(ctx_), 1); EXPECT_EQ(ut_txv_notify_late_last_delta(ctx_), 7u); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 0u); +} + +TEST_F(St20TxEpochTest, RepeatedLateFramesAccumulateExactDropsAndCallbacks) { + ut_txv_set_cur_epochs(ctx_, 2); + EXPECT_EQ(ut_txv_calc_frame_count_since_epoch(ctx_, kCurrentTai, 0), kCurrentEpoch); + ut_txv_set_cur_epochs(ctx_, kCurrentEpoch); + EXPECT_EQ(ut_txv_calc_frame_count_since_epoch(ctx_, kLateTai, 0), + kLateTai / kFramePeriodNs); + + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 9u); + EXPECT_EQ(ut_txv_notify_late_calls(ctx_), 2); + EXPECT_EQ(ut_txv_notify_late_last_delta(ctx_), 2u); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 0u); +} + +TEST_F(St20TxEpochTest, ValidRequiredTaiDoesNotChangeAnyStatistic) { + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); + uint64_t frame_count = + ut_txv_calc_frame_count_since_epoch(ctx_, kCurrentTai, kHalfFrameTargetTai); + + EXPECT_EQ(frame_count, (kHalfFrameTargetTai + kFramePeriodNs / 2) / kFramePeriodNs); + ExpectNoStatsOrLateCallback(); } TEST_F(St20TxEpochTest, RequiredTaiInThePastFlagsStat) { - ut_txv_set_cur_epochs(ctx_, 9); /* next=10, onward=0 (normal path) */ - uint64_t frame_count = ut_txv_calc_frame_count_since_epoch(ctx_, 10000000, 5000000); - EXPECT_EQ(frame_count, 5u); /* required_tai-derived value, not overridden */ + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); /* next=10, onward=0 (normal path) */ + uint64_t frame_count = ut_txv_calc_frame_count_since_epoch(ctx_, kCurrentTai, kPastTai); + EXPECT_EQ(frame_count, + kPastTai / kFramePeriodNs); /* required_tai-derived value, not overridden */ EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txv_notify_late_calls(ctx_), 0); } TEST_F(St20TxEpochTest, RequiredTaiTooFarFutureFlagsStat) { - ut_txv_set_cur_epochs(ctx_, 9); /* current frame count = 10, limit = +1000 */ - uint64_t required_tai = 1011ull * 1000000; + ut_txv_set_cur_epochs(ctx_, + kInitialEpoch); /* current frame count = 10, limit = +1000 */ + uint64_t required_tai = kBeyondFutureBoundaryTai; uint64_t frame_count = - ut_txv_calc_frame_count_since_epoch(ctx_, 10000000, required_tai); - EXPECT_EQ(frame_count, 1011u); + ut_txv_calc_frame_count_since_epoch(ctx_, kCurrentTai, required_tai); + EXPECT_EQ(frame_count, required_tai / kFramePeriodNs); EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txv_notify_late_calls(ctx_), 0); } -TEST_F(St20TxEpochTest, RequiredTaiPreservedThroughOnwardResync) { - ut_txv_set_cur_epochs(ctx_, 1000000); /* forces the onward-resync branch */ - uint64_t frame_count = ut_txv_calc_frame_count_since_epoch(ctx_, 5000000, 2000000); - EXPECT_EQ(frame_count, 2u); /* required_tai-derived value survives the resync */ - EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u); - EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 999996u); +TEST_F(St20TxEpochTest, AppTargetDoesNotReportStaleOnwardState) { + constexpr uint64_t target_tai = 12 * kFramePeriodNs; + ut_txv_set_cur_epochs(ctx_, kStaleEpoch); + uint64_t frame_count = + ut_txv_calc_frame_count_since_epoch(ctx_, kCurrentTai, target_tai); + EXPECT_EQ(frame_count, target_tai / kFramePeriodNs); + ExpectNoStatsOrLateCallback(); } TEST_F(St20TxEpochTest, RequiredTaiExactlyAtInThePastBoundaryDoesNotFlag) { - ut_txv_set_cur_epochs(ctx_, 9); /* current frame count = 10 */ - ut_txv_calc_frame_count_since_epoch(ctx_, 10000000, - 10000000); /* required frame count == 10 */ + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); /* current frame count = 10 */ + ut_txv_calc_frame_count_since_epoch(ctx_, kCurrentTai, + kCurrentTai); /* required frame count == 10 */ EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 0u); } TEST_F(St20TxEpochTest, RequiredTaiExactlyAtFutureBoundaryDoesNotFlag) { - ut_txv_set_cur_epochs(ctx_, 9); /* current frame count = 10, limit = +1000 */ - ut_txv_calc_frame_count_since_epoch(ctx_, 10000000, - 1010000000ull); /* required frame count == 1010 */ + ut_txv_set_cur_epochs(ctx_, + kInitialEpoch); /* current frame count = 10, limit = +1000 */ + ut_txv_calc_frame_count_since_epoch( + ctx_, kCurrentTai, kFutureBoundaryTai); /* required frame count == 1010 */ EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 0u); } -TEST_F(St20TxEpochTest, CurEpochsWraparoundIsWellDefinedNotUB) { - ut_txv_set_cur_epochs(ctx_, UINT64_MAX); /* next_free_frame_slot wraps to 0 */ - uint64_t frame_count = ut_txv_calc_frame_count_since_epoch(ctx_, 5000000, 0); - EXPECT_EQ(frame_count, 5u); - EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 5u); +TEST_F(St20TxEpochTest, RepeatedInvalidTimestampsAccumulateWithoutEpochRecovery) { + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); + EXPECT_EQ(ut_txv_calc_frame_count_since_epoch(ctx_, kCurrentTai, kPastTai), + kPastTai / kFramePeriodNs); + EXPECT_EQ( + ut_txv_calc_frame_count_since_epoch(ctx_, kCurrentTai, kBeyondFutureBoundaryTai), + kBeyondFutureBoundaryTai / kFramePeriodNs); + + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 2u); EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txv_notify_late_calls(ctx_), 0); +} + +TEST_F(St20TxEpochTest, CurEpochsWraparoundResyncsWithoutRecoveryStats) { + ut_txv_set_cur_epochs(ctx_, UINT64_MAX); + uint64_t frame_count = ut_txv_calc_frame_count_since_epoch(ctx_, kPastTai, 0); + EXPECT_EQ(frame_count, kPastTai / kFramePeriodNs); + ExpectNoStatsOrLateCallback(); } TEST_F(St20TxEpochTest, CurTaiNearUint64MaxNoCrash) { ut_txv_set_cur_epochs(ctx_, 0); /* next_free_frame_slot=1 */ uint64_t cur_tai = UINT64_MAX; - uint64_t expected_frame_count_tai = cur_tai / 1000000ull; + uint64_t expected_frame_count_tai = cur_tai / kFramePeriodNs; uint64_t frame_count = ut_txv_calc_frame_count_since_epoch(ctx_, cur_tai, 0); EXPECT_EQ(frame_count, expected_frame_count_tai); EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), expected_frame_count_tai - 1); } -/* Reproduces the required_tai asymmetry flagged in review: unlike the onward - * branch (which now preserves a validated required_tai through resync, see - * RequiredTaiPreservedThroughOnwardResync above), the late (else) branch has - * no `if (!required_tai)` guard -- it unconditionally overwrites frame_count - * with frame_count_tai, silently discarding a validated required_tai. This - * asserts the CORRECT/expected behavior (symmetric with the onward branch) - * and currently FAILS against production code, confirming the defect is - * real rather than hypothetical. */ -TEST_F(St20TxEpochTest, LatePathShouldPreserveRequiredTaiLikeOnwardBranch) { - ut_txv_set_cur_epochs(ctx_, 2); /* next_free_frame_slot=3 */ - uint64_t frame_count = ut_txv_calc_frame_count_since_epoch(ctx_, 10000000, 4000000); - EXPECT_EQ(frame_count, 4u) << "late branch should preserve the required_tai-derived " - "frame_count (4), not overwrite it with frame_count_tai " - "(10)"; - EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u); - EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 7u); +TEST_F(St20TxEpochTest, AppTargetDoesNotReportStaleLateState) { + ut_txv_set_cur_epochs(ctx_, 2); + uint64_t frame_count = + ut_txv_calc_frame_count_since_epoch(ctx_, kCurrentTai, kLateTargetTai); + EXPECT_EQ(frame_count, kLateTargetTai / kFramePeriodNs); + ExpectNoStatsOrLateCallback(); } -/* Reproduces a second, independent gap: validate_user_timestamp() flags an - * out-of-range required_tai (e.g. a caller unit-conversion bug -- passing - * milliseconds where nanoseconds are expected) via a stat counter, but the - * wildly-wrong derived frame_count is still returned and used verbatim -- - * there is no fallback to real time once validation fails. Contrast with - * LargePtpStepResyncsInOneCall, where a bad cur_epochs DOES self-heal via - * the onward-resync branch: here, an out-of-range *required_tai* has no - * equivalent self-healing path in the normal (non-onward, non-late) branch. - * This asserts the safer/expected fallback behavior and currently FAILS. */ -TEST_F(St20TxEpochTest, InvalidRequiredTaiIsFlaggedButNotClampedToRealTime) { +TEST_F(St20TxEpochTest, RequiredTaiRoundingToZeroFallsBackToCurrentTime) { ut_txv_set_cur_epochs(ctx_, 9); /* next_free_frame_slot=10, onward=0 (normal path) */ - uint64_t required_tai = 10; /* caller bug: ms instead of ns, ~1e6x too small */ + uint64_t required_tai = 10; uint64_t frame_count = - ut_txv_calc_frame_count_since_epoch(ctx_, 10000000, required_tai); + ut_txv_calc_frame_count_since_epoch(ctx_, kCurrentTai, required_tai); - EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u) - << "validation should flag the out-of-range required_tai"; - EXPECT_EQ(frame_count, 10u) << "an out-of-range required_tai should fall back to " - "real time (frame 10), not be honored verbatim (frame " - << frame_count << ")"; + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(frame_count, kCurrentEpoch); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txv_notify_late_calls(ctx_), 0); } diff --git a/tests/unit/session/st20_tx/pacing_test.cpp b/tests/unit/session/st20_tx/pacing_test.cpp index 2d4768d54..337f9c6c0 100644 --- a/tests/unit/session/st20_tx/pacing_test.cpp +++ b/tests/unit/session/st20_tx/pacing_test.cpp @@ -13,54 +13,400 @@ #include "session/st20_tx_harness.h" +namespace { +constexpr uint64_t kNanosecondsPerMillisecond = 1000 * 1000; +constexpr uint64_t kNanosecondsPerSecond = 1000 * kNanosecondsPerMillisecond; +constexpr uint64_t kFramePeriodNs = kNanosecondsPerMillisecond; +constexpr uint64_t kCurrentTai = 10 * kFramePeriodNs; +constexpr uint64_t kCurrentTsc = kFramePeriodNs / 2; +constexpr uint64_t kCurrentEpoch = kCurrentTai / kFramePeriodNs; +constexpr uint64_t kInitialEpoch = kCurrentEpoch - 1; +constexpr uint64_t kStaleEpoch = 1000; +constexpr uint64_t kTargetTai = kCurrentTai + kFramePeriodNs / 2; +constexpr uint64_t kAlignedTargetTai = kCurrentTai + kFramePeriodNs; +constexpr uint64_t kPastTai = kCurrentTai - kFramePeriodNs / 2; +constexpr uint64_t kTrOffsetNs = kNanosecondsPerMillisecond / 1000; +constexpr uint64_t kPacketIntervalNs = kTrOffsetNs / 10; +constexpr uint32_t kVirtualReceiverBufferPackets = 2; +constexpr uint64_t kReceiverScheduleOffsetNs = + kTrOffsetNs - kVirtualReceiverBufferPackets * kPacketIntervalNs; +constexpr uint64_t kInvalidMediaClockTimestamp = 10 * ST10_VIDEO_SAMPLING_RATE_90K; +} // namespace + class St20TxSyncPacingTest : public ::testing::Test { protected: void SetUp() override { ASSERT_EQ(ut_txv_init(), 0); ctx_ = ut_txv_create(); ASSERT_NE(ctx_, nullptr); - ut_txv_set_frame_time(ctx_, 1000000.0L); + ut_txv_set_frame_time(ctx_, kFramePeriodNs); ut_txv_set_max_onward_epochs(ctx_, 3); } void TearDown() override { ut_txv_destroy(ctx_); } + void ExpectNoPacingStats() { + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_mismatch(ctx_), 0u); + EXPECT_EQ(ut_txv_notify_late_calls(ctx_), 0); + } ut_txv_ctx* ctx_ = nullptr; }; TEST_F(St20TxSyncPacingTest, NormalCursorDerivation) { - ut_txv_set_cur_epochs(ctx_, 9); /* steady state -> frame_count becomes 10 */ - ut_txv_set_tr_offset(ctx_, 1000.0L); - ut_txv_set_vrx(ctx_, 2); - ut_txv_set_trs(ctx_, 100.0L); - ut_txv_set_mock_ptp_time(ctx_, 10000000); - ut_txv_set_mock_tsc_time(ctx_, 500000); + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); /* steady state -> frame_count becomes 10 */ + ut_txv_set_tr_offset(ctx_, kTrOffsetNs); + ut_txv_set_vrx(ctx_, kVirtualReceiverBufferPackets); + ut_txv_set_trs(ctx_, kPacketIntervalNs); + ut_txv_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txv_set_mock_tsc_time(ctx_, kCurrentTsc); ASSERT_EQ(ut_txv_sync_pacing(ctx_, 0), 0); - EXPECT_EQ(ut_txv_cur_epochs(ctx_), 10u); - EXPECT_DOUBLE_EQ((double)ut_txv_ptp_time_cursor(ctx_), 10000800.0); - EXPECT_DOUBLE_EQ((double)ut_txv_tsc_time_cursor(ctx_), 500800.0); - EXPECT_EQ(ut_txv_tsc_time_frame_start(ctx_), 500800u); + EXPECT_EQ(ut_txv_cur_epochs(ctx_), kCurrentEpoch); + EXPECT_EQ(ut_txv_ptp_time_cursor(ctx_), kCurrentTai + kReceiverScheduleOffsetNs); + EXPECT_EQ(ut_txv_tsc_time_cursor(ctx_), kCurrentTsc + kReceiverScheduleOffsetNs); + EXPECT_EQ(ut_txv_tsc_time_frame_start(ctx_), kCurrentTsc + kReceiverScheduleOffsetNs); + ExpectNoPacingStats(); } TEST_F(St20TxSyncPacingTest, ExactUserPacingBypassesTransmissionStartTime) { - ut_txv_set_cur_epochs(ctx_, 9); + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); + ut_txv_set_user_pacing(ctx_, true); + ut_txv_set_exact_user_pacing(ctx_, true); + ut_txv_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txv_set_mock_tsc_time(ctx_, kCurrentTsc); + + ASSERT_EQ(ut_txv_sync_pacing(ctx_, kTargetTai), 0); + + EXPECT_EQ(ut_txv_ptp_time_cursor(ctx_), kTargetTai); + EXPECT_EQ(ut_txv_tsc_time_cursor(ctx_), kCurrentTsc + kTargetTai - kCurrentTai); + ExpectNoPacingStats(); +} + +TEST_F(St20TxSyncPacingTest, TimestampIsIgnoredWithoutUserPacing) { + EXPECT_EQ(ut_txv_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_TAI, kTargetTai), 0u); + ExpectNoPacingStats(); +} + +TEST_F(St20TxSyncPacingTest, ZeroTimestampWithoutExactPacingFallsBackWithoutError) { + ut_txv_set_user_pacing(ctx_, true); + + EXPECT_EQ(ut_txv_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_TAI, 0), 0u); + ExpectNoPacingStats(); +} + +TEST_F(St20TxSyncPacingTest, UserPacingAlignsTimestampToReceiverSchedule) { + ut_txv_set_user_pacing(ctx_, true); + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); + ut_txv_set_tr_offset(ctx_, kTrOffsetNs); + ut_txv_set_vrx(ctx_, kVirtualReceiverBufferPackets); + ut_txv_set_trs(ctx_, kPacketIntervalNs); + ut_txv_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txv_set_mock_tsc_time(ctx_, kCurrentTsc); + + uint64_t required_tai = + ut_txv_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_TAI, kTargetTai); + ASSERT_EQ(required_tai, kTargetTai); + ASSERT_EQ(ut_txv_sync_pacing(ctx_, required_tai), 0); + + EXPECT_EQ(ut_txv_cur_epochs(ctx_), kAlignedTargetTai / kFramePeriodNs); + EXPECT_EQ(ut_txv_ptp_time_cursor(ctx_), kAlignedTargetTai + kReceiverScheduleOffsetNs); + EXPECT_EQ(ut_txv_tsc_time_cursor(ctx_), + kCurrentTsc + kAlignedTargetTai + kReceiverScheduleOffsetNs - kCurrentTai); + ExpectNoPacingStats(); +} + +TEST_F(St20TxSyncPacingTest, FrameTaskletUserPacingAlignsFirstPacketTarget) { + ut_txv_set_user_pacing(ctx_, true); + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); + ut_txv_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txv_set_mock_tsc_time(ctx_, kCurrentTsc); + uint64_t packet_tsc = 0; + uint64_t packet_ptp = 0; + + ASSERT_EQ(ut_txv_run_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_TAI, kTargetTai, + &packet_tsc, &packet_ptp), + 0); + + EXPECT_EQ(ut_txv_get_next_frame_calls(ctx_), 1); + EXPECT_EQ(ut_txv_notify_frame_done_calls(ctx_), 1); + EXPECT_EQ(ut_txv_notify_frame_done_idx(ctx_), 0u); + EXPECT_EQ(ut_txv_notify_frame_done_timestamp(ctx_), kAlignedTargetTai); + EXPECT_EQ(ut_txv_notify_frame_done_epoch(ctx_), kAlignedTargetTai / kFramePeriodNs); + EXPECT_TRUE(ut_txv_frame_is_waiting(ctx_)); + EXPECT_EQ(ut_txv_frame_refcnt(ctx_), 0); + EXPECT_EQ(packet_tsc, kCurrentTsc + kAlignedTargetTai - kCurrentTai); + EXPECT_EQ(packet_ptp, kAlignedTargetTai); + EXPECT_EQ(ut_txv_stat_port_build(ctx_), 1u); + EXPECT_EQ(ut_txv_stat_port_frames(ctx_), 1u); + EXPECT_EQ(ut_txv_stat_exceed_frame_time(ctx_), 0u); + ExpectNoPacingStats(); +} + +TEST_F(St20TxSyncPacingTest, FrameTaskletExactUserPacingUsesFirstPacketTargetVerbatim) { + ut_txv_set_user_pacing(ctx_, true); + ut_txv_set_exact_user_pacing(ctx_, true); + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); + ut_txv_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txv_set_mock_tsc_time(ctx_, kCurrentTsc); + uint64_t packet_tsc = 0; + uint64_t packet_ptp = 0; + + ASSERT_EQ(ut_txv_run_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_TAI, kTargetTai, + &packet_tsc, &packet_ptp), + 0); + + EXPECT_EQ(ut_txv_get_next_frame_calls(ctx_), 1); + EXPECT_EQ(ut_txv_notify_frame_done_calls(ctx_), 1); + EXPECT_EQ(ut_txv_notify_frame_done_idx(ctx_), 0u); + EXPECT_EQ(ut_txv_notify_frame_done_timestamp(ctx_), kTargetTai); + EXPECT_EQ(ut_txv_notify_frame_done_epoch(ctx_), + (kTargetTai + kFramePeriodNs / 2) / kFramePeriodNs); + EXPECT_TRUE(ut_txv_frame_is_waiting(ctx_)); + EXPECT_EQ(ut_txv_frame_refcnt(ctx_), 0); + EXPECT_EQ(packet_tsc, kCurrentTsc + kTargetTai - kCurrentTai); + EXPECT_EQ(packet_ptp, kTargetTai); + EXPECT_EQ(ut_txv_stat_port_build(ctx_), 1u); + EXPECT_EQ(ut_txv_stat_port_frames(ctx_), 1u); + EXPECT_EQ(ut_txv_stat_exceed_frame_time(ctx_), 0u); + ExpectNoPacingStats(); +} + +TEST_F(St20TxSyncPacingTest, FrameTaskletLateRecoveryCountsExactSkippedSlots) { + ut_txv_set_cur_epochs(ctx_, 2); + ut_txv_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txv_set_mock_tsc_time(ctx_, kCurrentTsc); + uint64_t packet_tsc = 0; + uint64_t packet_ptp = 0; + + ASSERT_EQ( + ut_txv_run_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_TAI, 0, &packet_tsc, &packet_ptp), + 0); + + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 7u); + EXPECT_EQ(ut_txv_notify_late_calls(ctx_), 1); + EXPECT_EQ(ut_txv_notify_late_last_delta(ctx_), 7u); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_mismatch(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_port_build(ctx_), 1u); + EXPECT_EQ(ut_txv_stat_port_frames(ctx_), 1u); +} + +TEST_F(St20TxSyncPacingTest, FrameTaskletOnwardRecoveryCountsExactGap) { + constexpr uint64_t onward_recovery_tai = 6 * kFramePeriodNs; + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); + ut_txv_set_mock_ptp_time(ctx_, onward_recovery_tai); + ut_txv_set_mock_tsc_time(ctx_, kCurrentTsc); + uint64_t packet_tsc = 0; + uint64_t packet_ptp = 0; + + ASSERT_EQ( + ut_txv_run_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_TAI, 0, &packet_tsc, &packet_ptp), + 0); + + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 4u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_mismatch(ctx_), 0u); + EXPECT_EQ(ut_txv_notify_late_calls(ctx_), 0); + EXPECT_EQ(packet_ptp, onward_recovery_tai); + EXPECT_EQ(ut_txv_stat_port_build(ctx_), 1u); + EXPECT_EQ(ut_txv_stat_port_frames(ctx_), 1u); +} + +TEST_F(St20TxSyncPacingTest, FrameTaskletInvalidTimestampStillBuildsFrame) { + constexpr uint64_t invalid_past_tai = 5 * kFramePeriodNs; + ut_txv_set_user_pacing(ctx_, true); + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); + ut_txv_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txv_set_mock_tsc_time(ctx_, kCurrentTsc); + uint64_t packet_tsc = 0; + uint64_t packet_ptp = 0; + + ASSERT_EQ(ut_txv_run_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_TAI, invalid_past_tai, + &packet_tsc, &packet_ptp), + 0); + + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_mismatch(ctx_), 0u); + EXPECT_EQ(ut_txv_notify_late_calls(ctx_), 0); + EXPECT_EQ(ut_txv_notify_frame_done_calls(ctx_), 1); + EXPECT_TRUE(ut_txv_frame_is_waiting(ctx_)); + EXPECT_EQ(ut_txv_stat_port_build(ctx_), 1u); + EXPECT_EQ(ut_txv_stat_port_frames(ctx_), 1u); +} + +TEST_F(St20TxSyncPacingTest, ExactPastHalfFrameTimestampCountsOneError) { + ut_txv_set_user_pacing(ctx_, true); + ut_txv_set_exact_user_pacing(ctx_, true); + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); + ut_txv_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txv_set_mock_tsc_time(ctx_, kCurrentTsc); + + ASSERT_EQ(ut_txv_sync_pacing(ctx_, kPastTai), 0); + + EXPECT_EQ(ut_txv_stat_epoch_mismatch(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txv_notify_late_calls(ctx_), 0); +} + +TEST_F(St20TxSyncPacingTest, ExactTimestampOneNanosecondPastCountsOneError) { + ut_txv_set_user_pacing(ctx_, true); + ut_txv_set_exact_user_pacing(ctx_, true); + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); + ut_txv_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txv_set_mock_tsc_time(ctx_, kCurrentTsc); + + ASSERT_EQ(ut_txv_sync_pacing(ctx_, kCurrentTai - 1), 0); + + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txv_tsc_time_cursor(ctx_), kCurrentTsc); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txv_notify_late_calls(ctx_), 0); +} + +TEST_F(St20TxSyncPacingTest, ExactTimestampAtCurrentTimeIsValid) { + ut_txv_set_user_pacing(ctx_, true); + ut_txv_set_exact_user_pacing(ctx_, true); + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); + ut_txv_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txv_set_mock_tsc_time(ctx_, kCurrentTsc); + + ASSERT_EQ(ut_txv_sync_pacing(ctx_, kCurrentTai), 0); + + ExpectNoPacingStats(); +} + +TEST_F(St20TxSyncPacingTest, ExactTimestampOneNanosecondFutureIsValid) { + ut_txv_set_user_pacing(ctx_, true); + ut_txv_set_exact_user_pacing(ctx_, true); + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); + ut_txv_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txv_set_mock_tsc_time(ctx_, kCurrentTsc); + + ASSERT_EQ(ut_txv_sync_pacing(ctx_, kCurrentTai + 1), 0); + + EXPECT_EQ(ut_txv_tsc_time_cursor(ctx_), kCurrentTsc + 1); + ExpectNoPacingStats(); +} + +TEST_F(St20TxSyncPacingTest, ConsecutiveUserTimestampsReplaceInternalEpochState) { + constexpr uint64_t first_target_tai = 12 * kFramePeriodNs; + constexpr uint64_t second_target_tai = 13 * kFramePeriodNs; + ut_txv_set_user_pacing(ctx_, true); + ut_txv_set_cur_epochs(ctx_, kStaleEpoch); + ut_txv_set_mock_tsc_time(ctx_, kCurrentTsc); + + ut_txv_set_mock_ptp_time(ctx_, kCurrentTai); + ASSERT_EQ(ut_txv_sync_pacing(ctx_, first_target_tai), 0); + EXPECT_EQ(ut_txv_cur_epochs(ctx_), first_target_tai / kFramePeriodNs); + + ut_txv_set_mock_ptp_time(ctx_, kAlignedTargetTai); + ASSERT_EQ(ut_txv_sync_pacing(ctx_, second_target_tai), 0); + EXPECT_EQ(ut_txv_cur_epochs(ctx_), second_target_tai / kFramePeriodNs); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 0u); + EXPECT_EQ(ut_txv_notify_late_calls(ctx_), 0); +} + +TEST_F(St20TxSyncPacingTest, InvalidMediaClockTimestampFallsBackToDefaultPacing) { + ut_txv_set_user_pacing(ctx_, true); + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); + ut_txv_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txv_set_mock_tsc_time(ctx_, kCurrentTsc); + + uint64_t required_tai = ut_txv_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, + kInvalidMediaClockTimestamp); + ASSERT_EQ(required_tai, 0u); + ASSERT_EQ(ut_txv_sync_pacing(ctx_, required_tai), 0); + + EXPECT_EQ(ut_txv_cur_epochs(ctx_), kCurrentEpoch); + EXPECT_EQ(ut_txv_ptp_time_cursor(ctx_), kCurrentTai); + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_mismatch(ctx_), 0u); + EXPECT_EQ(ut_txv_notify_late_calls(ctx_), 0); +} + +TEST_F(St20TxSyncPacingTest, ExactUnsupportedMediaClockCountsOneError) { + ut_txv_set_user_pacing(ctx_, true); + ut_txv_set_exact_user_pacing(ctx_, true); + + EXPECT_EQ(ut_txv_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, + kInvalidMediaClockTimestamp), + 0u); + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u); +} + +TEST_F(St20TxSyncPacingTest, ExactTimestampNearUint64MaxSaturatesTscTarget) { + ut_txv_set_user_pacing(ctx_, true); + ut_txv_set_exact_user_pacing(ctx_, true); + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); + ut_txv_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txv_set_mock_tsc_time(ctx_, 2 * kCurrentTai); + + ASSERT_EQ(ut_txv_sync_pacing(ctx_, UINT64_MAX), 0); + + EXPECT_EQ((uint64_t)ut_txv_tsc_time_cursor(ctx_), UINT64_MAX); + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); +} + +TEST_F(St20TxSyncPacingTest, AlignedTimestampNearUint64MaxDoesNotSendImmediately) { + ut_txv_set_user_pacing(ctx_, true); + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); + ut_txv_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txv_set_mock_tsc_time(ctx_, kCurrentTsc); + + ASSERT_EQ(ut_txv_sync_pacing(ctx_, UINT64_MAX), 0); + + EXPECT_GT(ut_txv_tsc_time_cursor(ctx_), kCurrentTsc); + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); +} + +TEST_F(St20TxSyncPacingTest, ExactTimestampAtInt64MaxRemainsInFuture) { + ut_txv_set_user_pacing(ctx_, true); ut_txv_set_exact_user_pacing(ctx_, true); - ut_txv_set_mock_ptp_time(ctx_, 10000000); - ut_txv_set_mock_tsc_time(ctx_, 500000); + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); + ut_txv_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txv_set_mock_tsc_time(ctx_, kCurrentTsc); + + ASSERT_EQ(ut_txv_sync_pacing(ctx_, INT64_MAX), 0); - ASSERT_EQ(ut_txv_sync_pacing(ctx_, 10500000), 0); + EXPECT_GT(ut_txv_tsc_time_cursor(ctx_), kCurrentTsc); + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); +} + +TEST_F(St20TxSyncPacingTest, AlignedTimestampAtInt64MaxRemainsInFuture) { + ut_txv_set_user_pacing(ctx_, true); + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); + ut_txv_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txv_set_mock_tsc_time(ctx_, kCurrentTsc); + + ASSERT_EQ(ut_txv_sync_pacing(ctx_, INT64_MAX), 0); - EXPECT_DOUBLE_EQ((double)ut_txv_ptp_time_cursor(ctx_), - 10500000.0); /* verbatim required_tai */ - EXPECT_DOUBLE_EQ((double)ut_txv_tsc_time_cursor(ctx_), 1000000.0); + EXPECT_GT(ut_txv_tsc_time_cursor(ctx_), kCurrentTsc); + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); } -/* tv_pacing_required_tai() is the frame-level-only helper that derives - * required_tai from app-supplied timestamps; EXACT_USER_PACING+timestamp=0 - * is flagged there, not in tv_sync_pacing() (which is also reached from the - * RTP-level path with required_tai=0 by design and must never flag). */ TEST_F(St20TxSyncPacingTest, ExactUserPacingWithZeroRequiredTaiShouldBeFlagged) { ut_txv_set_user_pacing(ctx_, true); ut_txv_set_exact_user_pacing(ctx_, true); @@ -70,36 +416,149 @@ TEST_F(St20TxSyncPacingTest, ExactUserPacingWithZeroRequiredTaiShouldBeFlagged) EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u) << "EXACT_USER_PACING with timestamp=0 should be flagged as an invalid " "request"; + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txv_notify_late_calls(ctx_), 0); +} + +TEST_F(St20TxSyncPacingTest, ExactUserPacingWithZeroTimestampFallsBackToDefaultPacing) { + ut_txv_set_user_pacing(ctx_, true); + ut_txv_set_exact_user_pacing(ctx_, true); + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); + ut_txv_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txv_set_mock_tsc_time(ctx_, kCurrentTsc); + + uint64_t required_tai = ut_txv_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_TAI, 0); + ASSERT_EQ(required_tai, 0u); + ASSERT_EQ(ut_txv_sync_pacing(ctx_, required_tai), 0); + + EXPECT_EQ(ut_txv_cur_epochs(ctx_), kCurrentEpoch); + EXPECT_EQ(ut_txv_ptp_time_cursor(ctx_), kCurrentTai); + EXPECT_EQ(ut_txv_tsc_time_cursor(ctx_), kCurrentTsc); + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txv_notify_late_calls(ctx_), 0); +} + +TEST_F(St20TxSyncPacingTest, FrameTaskletExactZeroTimestampUsesDefaultFirstPacketTarget) { + ut_txv_set_user_pacing(ctx_, true); + ut_txv_set_exact_user_pacing(ctx_, true); + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); + ut_txv_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txv_set_mock_tsc_time(ctx_, kCurrentTsc); + uint64_t packet_tsc = 0; + uint64_t packet_ptp = 0; + + ASSERT_EQ( + ut_txv_run_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_TAI, 0, &packet_tsc, &packet_ptp), + 0); + + EXPECT_EQ(ut_txv_get_next_frame_calls(ctx_), 1); + EXPECT_EQ(ut_txv_notify_frame_done_calls(ctx_), 1); + EXPECT_EQ(ut_txv_notify_frame_done_timestamp(ctx_), kCurrentTai); + EXPECT_EQ(ut_txv_notify_frame_done_epoch(ctx_), kCurrentEpoch); + EXPECT_TRUE(ut_txv_frame_is_waiting(ctx_)); + EXPECT_EQ(ut_txv_frame_refcnt(ctx_), 0); + EXPECT_EQ(packet_tsc, kCurrentTsc); + EXPECT_EQ(packet_ptp, kCurrentTai); + EXPECT_EQ(ut_txv_stat_port_build(ctx_), 1u); + EXPECT_EQ(ut_txv_stat_port_frames(ctx_), 1u); + EXPECT_EQ(ut_txv_stat_exceed_frame_time(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txv_notify_late_calls(ctx_), 0); } -/* RTP-level callers invoke tv_sync_pacing() directly with required_tai=0, - * bypassing tv_pacing_required_tai() entirely, even when EXACT_USER_PACING - * is set (a session with type=RTP_LEVEL + USER_PACING|EXACT_USER_PACING - * passes tv_ops_check()). This must never flag on every frame. */ TEST_F(St20TxSyncPacingTest, RtpLevelExactUserPacingWithZeroRequiredTaiNeverFlags) { - ut_txv_set_cur_epochs(ctx_, 9); + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); ut_txv_set_exact_user_pacing(ctx_, true); - ut_txv_set_mock_ptp_time(ctx_, 10000000); - ut_txv_set_mock_tsc_time(ctx_, 500000); + ut_txv_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txv_set_mock_tsc_time(ctx_, kCurrentTsc); ASSERT_EQ(ut_txv_sync_pacing(ctx_, /*required_tai=*/0), 0); EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 0u) << "tv_sync_pacing() alone (the RTP-level call path) must never flag " "stat_error_user_timestamp regardless of EXACT_USER_PACING"; + EXPECT_EQ(ut_txv_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txv_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txv_notify_late_calls(ctx_), 0); } TEST_F(St20TxSyncPacingTest, NegativeTimeToTxClampsToZero) { - ut_txv_set_cur_epochs(ctx_, 9); /* steady state -> frame_count becomes 10 */ + constexpr uint32_t large_vrx_packets = 2 * 1000; + ut_txv_set_cur_epochs(ctx_, kInitialEpoch); /* steady state -> frame_count becomes 10 */ ut_txv_set_tr_offset(ctx_, 0.0L); - ut_txv_set_vrx(ctx_, 2000); - ut_txv_set_trs(ctx_, 100.0L); /* vrx*trs=200000, pushes start time before cur_tai */ - ut_txv_set_mock_ptp_time(ctx_, 10000000); - ut_txv_set_mock_tsc_time(ctx_, 500000); + ut_txv_set_vrx(ctx_, large_vrx_packets); + ut_txv_set_trs(ctx_, kPacketIntervalNs); + ut_txv_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txv_set_mock_tsc_time(ctx_, kCurrentTsc); ASSERT_EQ(ut_txv_sync_pacing(ctx_, 0), 0); - EXPECT_DOUBLE_EQ((double)ut_txv_tsc_time_cursor(ctx_), - 500000.0); /* clamped: cur_tsc + 0 */ - EXPECT_EQ(ut_txv_tsc_time_frame_start(ctx_), 500000u); + EXPECT_EQ(ut_txv_tsc_time_cursor(ctx_), kCurrentTsc); + EXPECT_EQ(ut_txv_tsc_time_frame_start(ctx_), kCurrentTsc); + ExpectNoPacingStats(); +} + +class St20TxTransmitterBoundaryTest + : public St20TxSyncPacingTest, + public ::testing::WithParamInterface {}; + +TEST_P(St20TxTransmitterBoundaryTest, TargetOneNanosecondBelowOneSecondWaits) { + int bursts_before_target = -1; + int bursts_at_target = -1; + + ASSERT_EQ(ut_txv_run_transmitter_boundary(ctx_, GetParam(), kNanosecondsPerSecond - 1, + &bursts_before_target, &bursts_at_target), + 0); + EXPECT_EQ(bursts_before_target, 0); + EXPECT_EQ(bursts_at_target, 1); +} + +TEST_P(St20TxTransmitterBoundaryTest, TargetAtOneSecondWaits) { + int bursts_before_target = -1; + int bursts_at_target = -1; + + ASSERT_EQ(ut_txv_run_transmitter_boundary(ctx_, GetParam(), kNanosecondsPerSecond, + &bursts_before_target, &bursts_at_target), + 0); + EXPECT_EQ(bursts_before_target, 0); + EXPECT_EQ(bursts_at_target, 1); +} + +TEST_P(St20TxTransmitterBoundaryTest, TargetBeyondOneSecondDoesNotRemainPending) { + int bursts_before_target = -1; + int bursts_at_target = -1; + + ASSERT_EQ(ut_txv_run_transmitter_boundary(ctx_, GetParam(), kNanosecondsPerSecond + 1, + &bursts_before_target, &bursts_at_target), + 0); + EXPECT_EQ(bursts_before_target, 1); + EXPECT_EQ(bursts_at_target, 1); +} + +INSTANTIATE_TEST_SUITE_P(AllSoftwareWaitPaths, St20TxTransmitterBoundaryTest, + ::testing::Values(UT_TXV_PACING_TSC, UT_TXV_PACING_PTP, + UT_TXV_PACING_RL)); + +TEST_F(St20TxSyncPacingTest, ExactTargetBeyondOneSecondFallsBackBeforePacketBuild) { + uint64_t packet_tsc = 0; + uint64_t packet_ptp = 0; + ut_txv_set_user_pacing(ctx_, true); + ut_txv_set_exact_user_pacing(ctx_, true); + ut_txv_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txv_set_mock_tsc_time(ctx_, kCurrentTsc); + + ASSERT_EQ(ut_txv_run_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_TAI, + kCurrentTai + kNanosecondsPerSecond + 1, &packet_tsc, + &packet_ptp), + 0); + EXPECT_EQ(packet_tsc, kCurrentTsc); + EXPECT_EQ(packet_ptp, kCurrentTai); + EXPECT_EQ(ut_txv_notify_frame_done_timestamp(ctx_), kCurrentTai); + EXPECT_EQ(ut_txv_notify_frame_done_epoch(ctx_), kCurrentEpoch); + EXPECT_EQ(ut_txv_stat_error_user_timestamp(ctx_), 1u); } diff --git a/tests/unit/session/st20_tx_harness.c b/tests/unit/session/st20_tx_harness.c index 5e4a33107..450d8dc5a 100644 --- a/tests/unit/session/st20_tx_harness.c +++ b/tests/unit/session/st20_tx_harness.c @@ -35,11 +35,23 @@ struct ut_txv_ctx { struct mtl_main_impl impl; + struct st_tx_video_sessions_mgr mgr; struct st_tx_video_session_impl session; uint64_t mock_ptp_ns; uint64_t mock_tsc_ns; + enum st10_timestamp_fmt app_tfmt; + uint64_t app_timestamp; + int get_next_frame_calls; + int notify_frame_done_calls; + uint16_t notify_frame_done_idx; + struct st20_tx_frame_meta notify_frame_done_meta; + enum st21_tx_frame_status frame_status; + int frame_refcnt; int notify_late_calls; uint64_t notify_late_last_delta; + int burst_calls; + struct rte_mbuf* burst_packets[8]; + unsigned int burst_packets_count; }; #include "session/st20_tx_harness.h" @@ -64,11 +76,46 @@ static int ut_txv_notify_frame_late(void* priv, uint64_t epoch_skipped) { return 0; } +static int ut_txv_get_next_frame(void* priv, uint16_t* next_frame_idx, + struct st20_tx_frame_meta* meta) { + struct ut_txv_ctx* ctx = priv; + ctx->get_next_frame_calls++; + *next_frame_idx = 0; + meta->tfmt = ctx->app_tfmt; + meta->timestamp = ctx->app_timestamp; + return 0; +} + +static int ut_txv_notify_frame_done(void* priv, uint16_t frame_idx, + struct st20_tx_frame_meta* meta) { + struct ut_txv_ctx* ctx = priv; + ctx->notify_frame_done_calls++; + ctx->notify_frame_done_idx = frame_idx; + ctx->notify_frame_done_meta = *meta; + return 0; +} + /* Seam: from here on, calls to mt_get_tsc() written inside st_tx_video_session.c * resolve to our mock instead. The real mt_get_tsc() compiled above (via the * explicit mt_main.h include) is unaffected. */ #define mt_get_tsc ut_txv_tsc_time_fn #include "st2110/st_tx_video_session.c" +#define mt_txq_burst ut_txv_txq_burst +static struct ut_txv_ctx* ut_txv_active_burst_ctx; +static uint16_t ut_txv_txq_burst(struct mt_txq_entry* entry, struct rte_mbuf** tx_pkts, + uint16_t nb_pkts) { + (void)entry; + struct ut_txv_ctx* ctx = ut_txv_active_burst_ctx; + if (!ctx) return 0; + ctx->burst_calls++; + for (uint16_t i = 0; i < nb_pkts; i++) { + if (ctx->burst_packets_count < RTE_DIM(ctx->burst_packets)) + ctx->burst_packets[ctx->burst_packets_count++] = tx_pkts[i]; + } + return nb_pkts; +} +#include "st2110/st_video_transmitter.c" +#undef mt_txq_burst #undef mt_get_tsc /* ── init (delegates to common) ───────────────────────────────────────── */ @@ -86,14 +133,23 @@ ut_txv_ctx* ut_txv_create(void) { ctx->impl.type = MT_HANDLE_MAIN; ctx->impl.tsc_hz = rte_get_tsc_hz(); ctx->impl.inf[MTL_PORT_P].ptp_get_time_fn = ut_txv_ptp_time_fn; + ctx->mgr.parent = &ctx->impl; + ctx->mgr.max_idx = 1; + rte_spinlock_init(&ctx->mgr.mutex[0]); struct st_tx_video_session_impl* s = &ctx->session; s->impl = &ctx->impl; + s->mgr = &ctx->mgr; s->idx = 0; - s->pacing.frame_time = 1000000.0L; /* 1ms, round number for simple math */ + s->active = true; + s->pacing.frame_time = NS_PER_MS; s->pacing.max_onward_epochs = 3; + s->fps_tm.sampling_clock_rate = ST10_VIDEO_SAMPLING_RATE_90K; + s->ops.get_next_frame = ut_txv_get_next_frame; + s->ops.notify_frame_done = ut_txv_notify_frame_done; s->ops.notify_frame_late = ut_txv_notify_frame_late; s->ops.priv = ctx; + ctx->mgr.sessions[0] = s; return ctx; } @@ -166,18 +222,153 @@ uint64_t ut_txv_pacing_required_tai(ut_txv_ctx* ctx, enum st10_timestamp_fmt tfm return tv_pacing_required_tai(&ctx->session, tfmt, timestamp); } +int ut_txv_run_frame_tasklet(ut_txv_ctx* ctx, enum st10_timestamp_fmt tfmt, + uint64_t timestamp, uint64_t* packet_tsc, + uint64_t* packet_ptp) { + static unsigned int test_idx; + struct st_tx_video_session_impl* s = &ctx->session; + struct st_frame_trans frame = {0}; + struct rte_mbuf* packet = NULL; + char pool_name[RTE_MEMPOOL_NAMESIZE]; + char ring_name[RTE_RING_NAMESIZE]; + uint8_t frame_data[4] = {0}; + int ret = -1; + + snprintf(pool_name, sizeof(pool_name), "ut_txv_pool_%u", test_idx); + snprintf(ring_name, sizeof(ring_name), "ut_txv_ring_%u", test_idx++); + s->mbuf_mempool_hdr[MTL_SESSION_PORT_P] = + rte_pktmbuf_pool_create(pool_name, 32, 0, sizeof(struct mt_muf_priv_data), + RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id()); + s->ring[MTL_SESSION_PORT_P] = ut_ring_create(ring_name, 32); + if (!s->mbuf_mempool_hdr[MTL_SESSION_PORT_P] || !s->ring[MTL_SESSION_PORT_P]) goto out; + + frame.addr = frame_data; + frame.idx = 0; + frame.priv = s; + s->st20_frames = &frame; + s->st20_frames_cnt = 1; + s->st20_frame_size = sizeof(frame_data); + s->st20_fb_size = sizeof(frame_data); + s->st20_linesize = sizeof(frame_data); + s->st20_bytes_in_line = sizeof(frame_data); + s->st20_pkts_in_line = 1; + s->st20_pkt_len = sizeof(frame_data); + s->st20_total_pkts = 1; + s->st20_frame_stat = ST21_TX_STAT_WAIT_FRAME; + s->st20_pg.size = sizeof(frame_data); + s->st20_pg.coverage = 2; + s->bulk = 1; + s->tx_no_chain = true; + s->ops.num_port = 1; + s->ops.type = ST20_TYPE_FRAME_LEVEL; + s->ops.packing = ST20_PACKING_GPM_SL; + s->ops.width = 2; + s->ops.height = 1; + ctx->app_tfmt = tfmt; + ctx->app_timestamp = timestamp; + ctx->get_next_frame_calls = 0; + ctx->notify_frame_done_calls = 0; + + tvs_tasklet_handler(&ctx->mgr); + ctx->frame_status = s->st20_frame_stat; + ctx->frame_refcnt = rte_atomic32_read(&frame.refcnt); + if (rte_ring_sc_dequeue(s->ring[MTL_SESSION_PORT_P], (void**)&packet) < 0) goto out; + *packet_tsc = st_tx_mbuf_get_tsc(packet); + *packet_ptp = st_tx_mbuf_get_ptp(packet); + ret = 0; + +out: + rte_pktmbuf_free(packet); + ut_ring_drain(s->ring[MTL_SESSION_PORT_P]); + rte_ring_free(s->ring[MTL_SESSION_PORT_P]); + rte_mempool_free(s->mbuf_mempool_hdr[MTL_SESSION_PORT_P]); + s->ring[MTL_SESSION_PORT_P] = NULL; + s->mbuf_mempool_hdr[MTL_SESSION_PORT_P] = NULL; + s->st20_frames = NULL; + return ret; +} + +int ut_txv_run_transmitter_boundary(ut_txv_ctx* ctx, enum ut_txv_pacing_way way, + uint64_t delta_ns, int* bursts_before_target, + int* bursts_at_target) { + static unsigned int test_idx; + struct st_tx_video_session_impl* s = &ctx->session; + struct rte_mbuf* packet = NULL; + char pool_name[RTE_MEMPOOL_NAMESIZE]; + char ring_name[RTE_RING_NAMESIZE]; + const uint64_t now = NS_PER_MS / 2; + int ret = -1; + + snprintf(pool_name, sizeof(pool_name), "ut_txv_trs_pool_%u", test_idx); + snprintf(ring_name, sizeof(ring_name), "ut_txv_trs_ring_%u", test_idx++); + s->mbuf_mempool_hdr[MTL_SESSION_PORT_P] = + rte_pktmbuf_pool_create(pool_name, 32, 0, sizeof(struct mt_muf_priv_data), + RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id()); + s->ring[MTL_SESSION_PORT_P] = ut_ring_create(ring_name, 32); + if (!s->mbuf_mempool_hdr[MTL_SESSION_PORT_P] || !s->ring[MTL_SESSION_PORT_P]) goto out; + packet = rte_pktmbuf_alloc(s->mbuf_mempool_hdr[MTL_SESSION_PORT_P]); + if (!packet) goto out; + packet->pkt_len = 64; + st_tx_mbuf_set_idx(packet, 0); + st_tx_mbuf_set_tsc(packet, now + delta_ns); + st_tx_mbuf_set_ptp(packet, now + delta_ns); + if (rte_ring_sp_enqueue(s->ring[MTL_SESSION_PORT_P], packet) < 0) goto out; + packet = NULL; + + s->bulk = 1; + s->ops.num_port = 1; + s->queue[MTL_SESSION_PORT_P] = (struct mt_txq_entry*)ctx; + s->pacing_way[MTL_SESSION_PORT_P] = (enum st21_tx_pacing_way)way; + s->pacing.warm_pkts = 0; + s->pacing.trs = NS_PER_US; + s->last_burst_succ_time_tsc[MTL_SESSION_PORT_P] = now; + s->tx_hang_detect_time_thresh = UINT64_MAX; + ctx->mock_tsc_ns = now; + ctx->mock_ptp_ns = now; + ctx->burst_calls = 0; + ctx->burst_packets_count = 0; + ut_txv_active_burst_ctx = ctx; + st_video_resolve_pacing_tasklet(s, MTL_SESSION_PORT_P); + s->pacing_tasklet_func[MTL_SESSION_PORT_P](&ctx->impl, s, MTL_SESSION_PORT_P); + *bursts_before_target = ctx->burst_calls; + + ctx->mock_tsc_ns = now + delta_ns; + ctx->mock_ptp_ns = now + delta_ns; + s->pacing_tasklet_func[MTL_SESSION_PORT_P](&ctx->impl, s, MTL_SESSION_PORT_P); + *bursts_at_target = ctx->burst_calls; + ret = 0; + +out: + ut_txv_active_burst_ctx = NULL; + rte_pktmbuf_free(packet); + for (unsigned int i = 0; i < ctx->burst_packets_count; i++) + rte_pktmbuf_free(ctx->burst_packets[i]); + ctx->burst_packets_count = 0; + if (s->ring[MTL_SESSION_PORT_P]) { + ut_ring_drain(s->ring[MTL_SESSION_PORT_P]); + rte_ring_free(s->ring[MTL_SESSION_PORT_P]); + } + rte_mempool_free(s->mbuf_mempool_hdr[MTL_SESSION_PORT_P]); + s->ring[MTL_SESSION_PORT_P] = NULL; + s->mbuf_mempool_hdr[MTL_SESSION_PORT_P] = NULL; + s->queue[MTL_SESSION_PORT_P] = NULL; + s->trs_target_tsc[MTL_SESSION_PORT_P] = 0; + s->trs_inflight_num[MTL_SESSION_PORT_P] = 0; + return ret; +} + /* ── accessors ─────────────────────────────────────────────────────────── */ uint64_t ut_txv_cur_epochs(const ut_txv_ctx* ctx) { return ctx->session.pacing.cur_epochs; } -long double ut_txv_tsc_time_cursor(const ut_txv_ctx* ctx) { - return ctx->session.pacing.tsc_time_cursor; +uint64_t ut_txv_tsc_time_cursor(const ut_txv_ctx* ctx) { + return (uint64_t)ctx->session.pacing.tsc_time_cursor; } -long double ut_txv_ptp_time_cursor(const ut_txv_ctx* ctx) { - return ctx->session.pacing.ptp_time_cursor; +uint64_t ut_txv_ptp_time_cursor(const ut_txv_ctx* ctx) { + return (uint64_t)ctx->session.pacing.ptp_time_cursor; } uint64_t ut_txv_tsc_time_frame_start(const ut_txv_ctx* ctx) { @@ -196,6 +387,10 @@ uint64_t ut_txv_stat_error_user_timestamp(const ut_txv_ctx* ctx) { return ctx->session.port_user_stats.common.stat_error_user_timestamp; } +uint64_t ut_txv_stat_epoch_mismatch(const ut_txv_ctx* ctx) { + return ctx->session.port_user_stats.common.stat_epoch_mismatch; +} + int ut_txv_notify_late_calls(const ut_txv_ctx* ctx) { return ctx->notify_late_calls; } @@ -203,3 +398,43 @@ int ut_txv_notify_late_calls(const ut_txv_ctx* ctx) { uint64_t ut_txv_notify_late_last_delta(const ut_txv_ctx* ctx) { return ctx->notify_late_last_delta; } + +int ut_txv_get_next_frame_calls(const ut_txv_ctx* ctx) { + return ctx->get_next_frame_calls; +} + +int ut_txv_notify_frame_done_calls(const ut_txv_ctx* ctx) { + return ctx->notify_frame_done_calls; +} + +uint16_t ut_txv_notify_frame_done_idx(const ut_txv_ctx* ctx) { + return ctx->notify_frame_done_idx; +} + +uint64_t ut_txv_notify_frame_done_timestamp(const ut_txv_ctx* ctx) { + return ctx->notify_frame_done_meta.timestamp; +} + +uint64_t ut_txv_notify_frame_done_epoch(const ut_txv_ctx* ctx) { + return ctx->notify_frame_done_meta.epoch; +} + +bool ut_txv_frame_is_waiting(const ut_txv_ctx* ctx) { + return ctx->frame_status == ST21_TX_STAT_WAIT_FRAME; +} + +int ut_txv_frame_refcnt(const ut_txv_ctx* ctx) { + return ctx->frame_refcnt; +} + +uint64_t ut_txv_stat_port_build(const ut_txv_ctx* ctx) { + return ctx->session.port_user_stats.common.port[MTL_SESSION_PORT_P].build; +} + +uint64_t ut_txv_stat_port_frames(const ut_txv_ctx* ctx) { + return ctx->session.port_user_stats.common.port[MTL_SESSION_PORT_P].frames; +} + +uint64_t ut_txv_stat_exceed_frame_time(const ut_txv_ctx* ctx) { + return ctx->session.port_user_stats.common.stat_exceed_frame_time; +} diff --git a/tests/unit/session/st20_tx_harness.h b/tests/unit/session/st20_tx_harness.h index 95a743846..bd1d8510f 100644 --- a/tests/unit/session/st20_tx_harness.h +++ b/tests/unit/session/st20_tx_harness.h @@ -24,6 +24,12 @@ extern "C" { typedef struct ut_txv_ctx ut_txv_ctx; +enum ut_txv_pacing_way { + UT_TXV_PACING_RL = 1, + UT_TXV_PACING_TSC = 2, + UT_TXV_PACING_PTP = 4, +}; + /* Initialise the shared DPDK EAL. Idempotent — safe to call once per gtest * fixture SetUp(). Returns 0 on success, < 0 on failure. */ int ut_txv_init(void); @@ -59,17 +65,34 @@ uint64_t ut_txv_calc_frame_count_since_epoch(ut_txv_ctx* ctx, uint64_t cur_tai, int ut_txv_sync_pacing(ut_txv_ctx* ctx, uint64_t required_tai); uint64_t ut_txv_pacing_required_tai(ut_txv_ctx* ctx, enum st10_timestamp_fmt tfmt, uint64_t timestamp); +int ut_txv_run_frame_tasklet(ut_txv_ctx* ctx, enum st10_timestamp_fmt tfmt, + uint64_t timestamp, uint64_t* packet_tsc, + uint64_t* packet_ptp); +int ut_txv_run_transmitter_boundary(ut_txv_ctx* ctx, enum ut_txv_pacing_way way, + uint64_t delta_ns, int* bursts_before_target, + int* bursts_at_target); /* ── accessors ─────────────────────────────────────────────────────────── */ uint64_t ut_txv_cur_epochs(const ut_txv_ctx* ctx); -long double ut_txv_tsc_time_cursor(const ut_txv_ctx* ctx); -long double ut_txv_ptp_time_cursor(const ut_txv_ctx* ctx); +uint64_t ut_txv_tsc_time_cursor(const ut_txv_ctx* ctx); +uint64_t ut_txv_ptp_time_cursor(const ut_txv_ctx* ctx); uint64_t ut_txv_tsc_time_frame_start(const ut_txv_ctx* ctx); uint64_t ut_txv_stat_epoch_onward(const ut_txv_ctx* ctx); uint64_t ut_txv_stat_epoch_drop(const ut_txv_ctx* ctx); uint64_t ut_txv_stat_error_user_timestamp(const ut_txv_ctx* ctx); +uint64_t ut_txv_stat_epoch_mismatch(const ut_txv_ctx* ctx); int ut_txv_notify_late_calls(const ut_txv_ctx* ctx); uint64_t ut_txv_notify_late_last_delta(const ut_txv_ctx* ctx); +int ut_txv_get_next_frame_calls(const ut_txv_ctx* ctx); +int ut_txv_notify_frame_done_calls(const ut_txv_ctx* ctx); +uint16_t ut_txv_notify_frame_done_idx(const ut_txv_ctx* ctx); +uint64_t ut_txv_notify_frame_done_timestamp(const ut_txv_ctx* ctx); +uint64_t ut_txv_notify_frame_done_epoch(const ut_txv_ctx* ctx); +bool ut_txv_frame_is_waiting(const ut_txv_ctx* ctx); +int ut_txv_frame_refcnt(const ut_txv_ctx* ctx); +uint64_t ut_txv_stat_port_build(const ut_txv_ctx* ctx); +uint64_t ut_txv_stat_port_frames(const ut_txv_ctx* ctx); +uint64_t ut_txv_stat_exceed_frame_time(const ut_txv_ctx* ctx); #ifdef __cplusplus } diff --git a/tests/unit/session/st40_tx/pacing_test.cpp b/tests/unit/session/st40_tx/pacing_test.cpp new file mode 100644 index 000000000..543eebccf --- /dev/null +++ b/tests/unit/session/st40_tx/pacing_test.cpp @@ -0,0 +1,758 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + */ + +#include + +#include + +#include "session/st40_tx_harness.h" + +namespace { +constexpr uint64_t kNanosecondsPerMillisecond = 1000 * 1000; +constexpr uint64_t kNanosecondsPerSecond = 1000 * kNanosecondsPerMillisecond; +constexpr uint64_t kFramePeriodNs = kNanosecondsPerMillisecond; +constexpr uint64_t kCurrentTai = 10 * kFramePeriodNs; +constexpr uint64_t kCurrentTsc = kFramePeriodNs / 2; +constexpr uint64_t kCurrentEpoch = kCurrentTai / kFramePeriodNs; +constexpr uint64_t kInitialEpoch = kCurrentEpoch - 1; +constexpr uint64_t kTargetTai = kCurrentTai + kFramePeriodNs / 2; +constexpr uint64_t kAlignedTargetTai = kCurrentTai + kFramePeriodNs; +constexpr uint64_t kPastTai = kCurrentTai - kFramePeriodNs / 2; +constexpr uint64_t kMediaClockRate = ST10_VIDEO_SAMPLING_RATE_90K; +constexpr uint64_t kMediaClockModulus = static_cast(UINT32_MAX) + 1; +constexpr uint64_t kStaleEpoch = 1000; +constexpr uint64_t kWrapTestMediaClockEra = 30000; +constexpr uint64_t kZeroTimestampMediaClockEra = 37300; + +constexpr uint64_t MediaClockToNanoseconds(uint64_t ticks) { + return (static_cast<__uint128_t>(ticks) * kNanosecondsPerSecond + kMediaClockRate / 2) / + kMediaClockRate; +} + +constexpr uint64_t AlignToFrame(uint64_t tai) { + return ((tai + kFramePeriodNs / 2) / kFramePeriodNs) * kFramePeriodNs; +} +} // namespace + +class St40TxPacingTest : public ::testing::Test { + protected: + void SetUp() override { + ASSERT_EQ(ut_txa_init(), 0); + ctx_ = ut_txa_create(); + ASSERT_NE(ctx_, nullptr); + } + + void TearDown() override { + ut_txa_destroy(ctx_); + } + + void ExpectNoPacingStats() { + EXPECT_EQ(ut_txa_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_mismatch(ctx_), 0u); + EXPECT_EQ(ut_txa_notify_late_calls(ctx_), 0); + } + + ut_txa_ctx* ctx_ = nullptr; +}; + +TEST_F(St40TxPacingTest, SteadyStateAdvancesByOneEpoch) { + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + EXPECT_EQ(ut_txa_calc_epoch(ctx_, kCurrentTai, 0), kCurrentEpoch); + ExpectNoPacingStats(); +} + +TEST_F(St40TxPacingTest, OnwardBoundaryIsInclusive) { + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + EXPECT_EQ(ut_txa_calc_epoch(ctx_, 7 * kFramePeriodNs, 0), kCurrentEpoch); + ExpectNoPacingStats(); +} + +TEST_F(St40TxPacingTest, OnwardGapBeyondBoundaryResyncs) { + constexpr uint64_t onward_recovery_tai = 6 * kFramePeriodNs; + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + EXPECT_EQ(ut_txa_calc_epoch(ctx_, onward_recovery_tai, 0), + onward_recovery_tai / kFramePeriodNs); + EXPECT_EQ(ut_txa_stat_epoch_onward(ctx_), 4u); + EXPECT_EQ(ut_txa_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_mismatch(ctx_), 0u); + EXPECT_EQ(ut_txa_notify_late_calls(ctx_), 0); +} + +TEST_F(St40TxPacingTest, RepeatedOnwardRecoveryAccumulatesExactGaps) { + constexpr uint64_t first_recovery_tai = 6 * kFramePeriodNs; + constexpr uint64_t second_recovery_tai = 8 * kFramePeriodNs; + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + EXPECT_EQ(ut_txa_calc_epoch(ctx_, first_recovery_tai, 0), + first_recovery_tai / kFramePeriodNs); + ut_txa_set_cur_epochs(ctx_, 12); + EXPECT_EQ(ut_txa_calc_epoch(ctx_, second_recovery_tai, 0), + second_recovery_tai / kFramePeriodNs); + + EXPECT_EQ(ut_txa_stat_epoch_onward(ctx_), 9u); + EXPECT_EQ(ut_txa_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_mismatch(ctx_), 0u); + EXPECT_EQ(ut_txa_notify_late_calls(ctx_), 0); +} + +TEST_F(St40TxPacingTest, AppTargetDoesNotReportStaleOnwardState) { + ut_txa_set_cur_epochs(ctx_, kStaleEpoch); + constexpr uint64_t target_tai = 12 * kFramePeriodNs; + EXPECT_EQ(ut_txa_calc_epoch(ctx_, kCurrentTai, target_tai), + target_tai / kFramePeriodNs); + ExpectNoPacingStats(); +} + +TEST_F(St40TxPacingTest, AppTargetDoesNotReportStaleLateState) { + ut_txa_set_cur_epochs(ctx_, 2); + constexpr uint64_t target_tai = 100 * kFramePeriodNs; + EXPECT_EQ(ut_txa_calc_epoch(ctx_, kCurrentTai, target_tai), + target_tai / kFramePeriodNs); + ExpectNoPacingStats(); +} + +TEST_F(St40TxPacingTest, RepeatedLateFramesAccumulateExactDropsAndCallbacks) { + constexpr uint64_t late_tai = 13 * kFramePeriodNs; + ut_txa_set_cur_epochs(ctx_, 2); + EXPECT_EQ(ut_txa_calc_epoch(ctx_, kCurrentTai, 0), kCurrentEpoch); + ut_txa_set_cur_epochs(ctx_, kCurrentEpoch); + EXPECT_EQ(ut_txa_calc_epoch(ctx_, late_tai, 0), late_tai / kFramePeriodNs); + + EXPECT_EQ(ut_txa_stat_epoch_drop(ctx_), 9u); + EXPECT_EQ(ut_txa_notify_late_calls(ctx_), 2); + EXPECT_EQ(ut_txa_notify_late_last_delta(ctx_), 2u); + EXPECT_EQ(ut_txa_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_mismatch(ctx_), 0u); +} + +TEST_F(St40TxPacingTest, RequiredTaiRoundingToZeroFallsBackToCurrentEpoch) { + constexpr uint64_t subframe_target_tai = 10; + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + EXPECT_EQ(ut_txa_calc_epoch(ctx_, kCurrentTai, subframe_target_tai), kCurrentEpoch); + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txa_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_mismatch(ctx_), 0u); + EXPECT_EQ(ut_txa_notify_late_calls(ctx_), 0); +} + +TEST_F(St40TxPacingTest, FutureValidationBoundaryIsInclusive) { + constexpr uint64_t future_boundary_tai = kCurrentTai + kNanosecondsPerSecond; + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + EXPECT_EQ(ut_txa_calc_epoch(ctx_, kCurrentTai, future_boundary_tai), + future_boundary_tai / kFramePeriodNs); + ExpectNoPacingStats(); +} + +TEST_F(St40TxPacingTest, TimestampBeyondFutureBoundaryIsPreservedAndFlagged) { + constexpr uint64_t beyond_future_boundary_tai = + kCurrentTai + kNanosecondsPerSecond + kFramePeriodNs; + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + EXPECT_EQ(ut_txa_calc_epoch(ctx_, kCurrentTai, beyond_future_boundary_tai), + beyond_future_boundary_tai / kFramePeriodNs); + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txa_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_mismatch(ctx_), 0u); + EXPECT_EQ(ut_txa_notify_late_calls(ctx_), 0); +} + +TEST_F(St40TxPacingTest, RepeatedInvalidTimestampsAccumulateWithoutEpochRecovery) { + constexpr uint64_t invalid_past_tai = 5 * kFramePeriodNs; + constexpr uint64_t invalid_future_tai = + kCurrentTai + kNanosecondsPerSecond + kFramePeriodNs; + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + EXPECT_EQ(ut_txa_calc_epoch(ctx_, kCurrentTai, invalid_past_tai), + invalid_past_tai / kFramePeriodNs); + EXPECT_EQ(ut_txa_calc_epoch(ctx_, kCurrentTai, invalid_future_tai), + invalid_future_tai / kFramePeriodNs); + + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 2u); + EXPECT_EQ(ut_txa_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_mismatch(ctx_), 0u); + EXPECT_EQ(ut_txa_notify_late_calls(ctx_), 0); +} + +TEST_F(St40TxPacingTest, CurEpochWrapResyncsWithoutRecoveryStats) { + ut_txa_set_cur_epochs(ctx_, UINT64_MAX); + constexpr uint64_t current_tai = 5 * kFramePeriodNs; + EXPECT_EQ(ut_txa_calc_epoch(ctx_, current_tai, 0), current_tai / kFramePeriodNs); + ExpectNoPacingStats(); +} + +TEST_F(St40TxPacingTest, TimestampIsIgnoredWithoutUserPacing) { + EXPECT_EQ(ut_txa_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_TAI, kTargetTai), 0u); +} + +TEST_F(St40TxPacingTest, UserPacingAlignsTimestampToEpoch) { + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + + uint64_t required_tai = + ut_txa_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_TAI, kTargetTai); + ASSERT_EQ(ut_txa_sync_pacing(ctx_, required_tai), 0); + + EXPECT_EQ(ut_txa_cur_epochs(ctx_), kAlignedTargetTai / kFramePeriodNs); + EXPECT_EQ(ut_txa_ptp_time_cursor(ctx_), kAlignedTargetTai); + EXPECT_EQ(ut_txa_tsc_time_cursor(ctx_), kCurrentTsc + kAlignedTargetTai - kCurrentTai); +} + +TEST_F(St40TxPacingTest, FrameTaskletUserPacingAlignsFirstPacketTarget) { + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + uint64_t packet_tsc = 0; + + ASSERT_EQ( + ut_txa_run_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_TAI, kTargetTai, &packet_tsc), 0); + + EXPECT_EQ(ut_txa_get_next_frame_calls(ctx_), 1); + EXPECT_EQ(ut_txa_notify_frame_done_calls(ctx_), 1); + EXPECT_EQ(ut_txa_notify_frame_done_idx(ctx_), 0u); + EXPECT_TRUE(ut_txa_frame_is_waiting(ctx_)); + EXPECT_EQ(ut_txa_frame_refcnt(ctx_), 0); + EXPECT_EQ(packet_tsc, kCurrentTsc + kAlignedTargetTai - kCurrentTai); + EXPECT_EQ(ut_txa_stat_port_build(ctx_), 1u); + EXPECT_EQ(ut_txa_stat_port_packets(ctx_), 1u); + EXPECT_EQ(ut_txa_stat_port_bytes(ctx_), ut_txa_packet_len(ctx_)); + EXPECT_GT(ut_txa_packet_len(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_port_frames(ctx_), 1u); + EXPECT_EQ(ut_txa_stat_recoverable_error(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_unrecoverable_error(ctx_), 0u); + ExpectNoPacingStats(); +} + +TEST_F(St40TxPacingTest, FrameTaskletExactUserPacingUsesFirstPacketTargetVerbatim) { + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_exact_user_pacing(ctx_, true); + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + uint64_t packet_tsc = 0; + + ASSERT_EQ( + ut_txa_run_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_TAI, kTargetTai, &packet_tsc), 0); + + EXPECT_EQ(ut_txa_get_next_frame_calls(ctx_), 1); + EXPECT_EQ(ut_txa_notify_frame_done_calls(ctx_), 1); + EXPECT_TRUE(ut_txa_frame_is_waiting(ctx_)); + EXPECT_EQ(ut_txa_frame_refcnt(ctx_), 0); + EXPECT_EQ(packet_tsc, kCurrentTsc + kTargetTai - kCurrentTai); + EXPECT_EQ(ut_txa_stat_port_build(ctx_), 1u); + EXPECT_EQ(ut_txa_stat_port_packets(ctx_), 1u); + EXPECT_EQ(ut_txa_stat_port_bytes(ctx_), ut_txa_packet_len(ctx_)); + EXPECT_GT(ut_txa_packet_len(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_port_frames(ctx_), 1u); + ExpectNoPacingStats(); +} + +TEST_F(St40TxPacingTest, FrameTaskletLateRecoveryCountsExactSkippedSlots) { + ut_txa_set_cur_epochs(ctx_, 2); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + uint64_t packet_tsc = 0; + + ASSERT_EQ(ut_txa_run_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_TAI, 0, &packet_tsc), 0); + + EXPECT_EQ(ut_txa_stat_epoch_drop(ctx_), 7u); + EXPECT_EQ(ut_txa_notify_late_calls(ctx_), 1); + EXPECT_EQ(ut_txa_notify_late_last_delta(ctx_), 7u); + EXPECT_EQ(ut_txa_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_mismatch(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_port_build(ctx_), 1u); + EXPECT_EQ(ut_txa_stat_port_packets(ctx_), 1u); + EXPECT_EQ(ut_txa_stat_port_frames(ctx_), 1u); +} + +TEST_F(St40TxPacingTest, FrameTaskletOnwardRecoveryCountsExactGap) { + constexpr uint64_t onward_recovery_tai = 6 * kFramePeriodNs; + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + ut_txa_set_mock_ptp_time(ctx_, onward_recovery_tai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + uint64_t packet_tsc = 0; + + ASSERT_EQ(ut_txa_run_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_TAI, 0, &packet_tsc), 0); + + EXPECT_EQ(ut_txa_stat_epoch_onward(ctx_), 4u); + EXPECT_EQ(ut_txa_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_mismatch(ctx_), 0u); + EXPECT_EQ(ut_txa_notify_late_calls(ctx_), 0); + EXPECT_EQ(ut_txa_stat_port_build(ctx_), 1u); + EXPECT_EQ(ut_txa_stat_port_packets(ctx_), 1u); + EXPECT_EQ(ut_txa_stat_port_frames(ctx_), 1u); +} + +TEST_F(St40TxPacingTest, FrameTaskletInvalidTimestampStillBuildsFrame) { + constexpr uint64_t invalid_past_tai = 5 * kFramePeriodNs; + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + uint64_t packet_tsc = 0; + + ASSERT_EQ(ut_txa_run_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_TAI, invalid_past_tai, + &packet_tsc), + 0); + + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txa_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_mismatch(ctx_), 1u); + EXPECT_EQ(ut_txa_notify_late_calls(ctx_), 0); + EXPECT_EQ(ut_txa_notify_frame_done_calls(ctx_), 1); + EXPECT_TRUE(ut_txa_frame_is_waiting(ctx_)); + EXPECT_EQ(ut_txa_stat_port_build(ctx_), 1u); + EXPECT_EQ(ut_txa_stat_port_packets(ctx_), 1u); + EXPECT_EQ(ut_txa_stat_port_frames(ctx_), 1u); +} + +TEST_F(St40TxPacingTest, ExactUserPacingUsesTimestampVerbatim) { + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_exact_user_pacing(ctx_, true); + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + + ASSERT_EQ(ut_txa_sync_pacing(ctx_, kTargetTai), 0); + + EXPECT_EQ(ut_txa_ptp_time_cursor(ctx_), kTargetTai); + EXPECT_EQ(ut_txa_tsc_time_cursor(ctx_), kCurrentTsc + kTargetTai - kCurrentTai); + ExpectNoPacingStats(); +} + +TEST_F(St40TxPacingTest, ExactPastHalfFrameTimestampCountsErrorAndMismatch) { + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_exact_user_pacing(ctx_, true); + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + + ASSERT_EQ(ut_txa_sync_pacing(ctx_, kPastTai), 0); + + EXPECT_EQ(ut_txa_stat_epoch_mismatch(ctx_), 1u); + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txa_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txa_notify_late_calls(ctx_), 0); +} + +TEST_F(St40TxPacingTest, ExactTimestampOneNanosecondPastCountsErrorAndMismatch) { + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_exact_user_pacing(ctx_, true); + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + + ASSERT_EQ(ut_txa_sync_pacing(ctx_, kCurrentTai - 1), 0); + + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txa_stat_epoch_mismatch(ctx_), 1u); + EXPECT_EQ(ut_txa_tsc_time_cursor(ctx_), kCurrentTsc); + EXPECT_EQ(ut_txa_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_onward(ctx_), 0u); +} + +TEST_F(St40TxPacingTest, ExactTimestampAtCurrentTimeDoesNotCountMismatch) { + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_exact_user_pacing(ctx_, true); + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + + ASSERT_EQ(ut_txa_sync_pacing(ctx_, kCurrentTai), 0); + + ExpectNoPacingStats(); +} + +TEST_F(St40TxPacingTest, ExactTimestampOneNanosecondFutureIsValid) { + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_exact_user_pacing(ctx_, true); + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + + ASSERT_EQ(ut_txa_sync_pacing(ctx_, kCurrentTai + 1), 0); + + EXPECT_EQ(ut_txa_tsc_time_cursor(ctx_), kCurrentTsc + 1); + ExpectNoPacingStats(); +} + +TEST_F(St40TxPacingTest, RepeatedEpochMismatchAccumulates) { + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_exact_user_pacing(ctx_, true); + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + + ASSERT_EQ(ut_txa_sync_pacing(ctx_, kPastTai), 0); + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + ASSERT_EQ(ut_txa_sync_pacing(ctx_, kPastTai), 0); + + EXPECT_EQ(ut_txa_stat_epoch_mismatch(ctx_), 2u); + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 2u); + EXPECT_EQ(ut_txa_stat_epoch_onward(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txa_notify_late_calls(ctx_), 0); +} + +TEST_F(St40TxPacingTest, MediaClockUserPacingAlignsConvertedTimestamp) { + ut_txa_set_user_pacing(ctx_, true); + constexpr uint64_t media_clock_timestamp = 945; + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + + uint64_t required_tai = ut_txa_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, + media_clock_timestamp); + ASSERT_EQ(required_tai, MediaClockToNanoseconds(media_clock_timestamp)); + ASSERT_EQ(ut_txa_sync_pacing(ctx_, required_tai), 0); + + EXPECT_EQ(ut_txa_ptp_time_cursor(ctx_), kAlignedTargetTai); +} + +TEST_F(St40TxPacingTest, MediaClockExactUserPacingUsesConvertedTimestampVerbatim) { + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_exact_user_pacing(ctx_, true); + constexpr uint64_t media_clock_timestamp = 945; + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + + uint64_t required_tai = ut_txa_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, + media_clock_timestamp); + ASSERT_EQ(required_tai, MediaClockToNanoseconds(media_clock_timestamp)); + ASSERT_EQ(ut_txa_sync_pacing(ctx_, required_tai), 0); + + EXPECT_EQ(ut_txa_ptp_time_cursor(ctx_), MediaClockToNanoseconds(media_clock_timestamp)); +} + +TEST_F(St40TxPacingTest, MediaClockExactTaskletUnwrapsFutureAcrossUint32Wrap) { + constexpr uint64_t media_clock_era = kWrapTestMediaClockEra; + constexpr uint64_t current_ticks = + media_clock_era * kMediaClockModulus + UINT32_MAX - 9; + constexpr uint64_t target_ticks = (media_clock_era + 1) * kMediaClockModulus + 5; + constexpr uint64_t current_tai = MediaClockToNanoseconds(current_ticks); + constexpr uint64_t target_tai = MediaClockToNanoseconds(target_ticks); + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_exact_user_pacing(ctx_, true); + ut_txa_set_cur_epochs(ctx_, current_tai / kFramePeriodNs); + ut_txa_set_mock_ptp_time(ctx_, current_tai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + uint64_t packet_tsc = 0; + + ASSERT_EQ(ut_txa_run_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, 5, &packet_tsc), + 0); + + EXPECT_EQ(packet_tsc, kCurrentTsc + target_tai - current_tai); + EXPECT_EQ(ut_txa_ptp_time_cursor(ctx_), target_tai); + ExpectNoPacingStats(); +} + +TEST_F(St40TxPacingTest, MediaClockAlignedTaskletUnwrapsFutureAcrossUint32Wrap) { + constexpr uint64_t media_clock_era = kWrapTestMediaClockEra; + constexpr uint64_t current_ticks = + media_clock_era * kMediaClockModulus + UINT32_MAX - 9; + constexpr uint64_t target_ticks = (media_clock_era + 1) * kMediaClockModulus + 100; + constexpr uint64_t current_tai = MediaClockToNanoseconds(current_ticks); + constexpr uint64_t aligned_target_tai = + AlignToFrame(MediaClockToNanoseconds(target_ticks)); + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_cur_epochs(ctx_, current_tai / kFramePeriodNs); + ut_txa_set_mock_ptp_time(ctx_, current_tai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + uint64_t packet_tsc = 0; + + ASSERT_EQ( + ut_txa_run_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, 100, &packet_tsc), 0); + + EXPECT_EQ(packet_tsc, kCurrentTsc + aligned_target_tai - current_tai); + EXPECT_EQ(ut_txa_ptp_time_cursor(ctx_), aligned_target_tai); + ExpectNoPacingStats(); +} + +TEST_F(St40TxPacingTest, MediaClockExactTaskletFallsBackForPastWrappedTarget) { + constexpr uint64_t media_clock_era = kWrapTestMediaClockEra; + constexpr uint64_t current_ticks = (media_clock_era + 1) * kMediaClockModulus + 10; + constexpr uint64_t current_tai = MediaClockToNanoseconds(current_ticks); + constexpr uint64_t fallback_tai = ((current_tai / kFramePeriodNs) + 1) * kFramePeriodNs; + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_exact_user_pacing(ctx_, true); + ut_txa_set_cur_epochs(ctx_, current_tai / kFramePeriodNs); + ut_txa_set_mock_ptp_time(ctx_, current_tai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + uint64_t packet_tsc = 0; + + ASSERT_EQ(ut_txa_run_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, UINT32_MAX - 5, + &packet_tsc), + 0); + + EXPECT_EQ(packet_tsc, kCurrentTsc + fallback_tai - current_tai); + EXPECT_EQ(ut_txa_ptp_time_cursor(ctx_), fallback_tai); + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txa_notify_frame_done_epoch(ctx_), fallback_tai / kFramePeriodNs); + EXPECT_EQ(ut_txa_notify_frame_done_timestamp(ctx_), fallback_tai); + EXPECT_EQ(ut_txa_stat_epoch_mismatch(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_onward(ctx_), 0u); +} + +TEST_F(St40TxPacingTest, MediaClockZeroUnwrapsAfterUint32WrapForUserPacing) { + constexpr uint64_t media_clock_era = kZeroTimestampMediaClockEra; + constexpr uint64_t current_ticks = media_clock_era * kMediaClockModulus + UINT32_MAX; + constexpr uint64_t target_ticks = (media_clock_era + 1) * kMediaClockModulus; + constexpr uint64_t current_tai = MediaClockToNanoseconds(current_ticks); + constexpr uint64_t target_tai = MediaClockToNanoseconds(target_ticks); + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_mock_ptp_time(ctx_, current_tai); + + EXPECT_EQ(ut_txa_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, 0), + target_tai); + ExpectNoPacingStats(); +} + +TEST_F(St40TxPacingTest, MediaClockZeroUnwrapsAfterUint32WrapForExactPacing) { + constexpr uint64_t media_clock_era = kZeroTimestampMediaClockEra; + constexpr uint64_t current_ticks = media_clock_era * kMediaClockModulus + UINT32_MAX; + constexpr uint64_t target_ticks = (media_clock_era + 1) * kMediaClockModulus; + constexpr uint64_t current_tai = MediaClockToNanoseconds(current_ticks); + constexpr uint64_t target_tai = MediaClockToNanoseconds(target_ticks); + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_exact_user_pacing(ctx_, true); + ut_txa_set_mock_ptp_time(ctx_, current_tai); + + EXPECT_EQ(ut_txa_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, 0), + target_tai); + ExpectNoPacingStats(); +} + +TEST_F(St40TxPacingTest, MediaClockZeroExactTaskletUsesWrappedFirstPacketTarget) { + constexpr uint64_t media_clock_era = kZeroTimestampMediaClockEra; + constexpr uint64_t current_ticks = media_clock_era * kMediaClockModulus + UINT32_MAX; + constexpr uint64_t target_ticks = (media_clock_era + 1) * kMediaClockModulus; + constexpr uint64_t current_tai = MediaClockToNanoseconds(current_ticks); + constexpr uint64_t target_tai = MediaClockToNanoseconds(target_ticks); + constexpr uint64_t target_epoch = (target_tai + kFramePeriodNs / 2) / kFramePeriodNs; + uint64_t packet_tsc = 0; + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_exact_user_pacing(ctx_, true); + ut_txa_set_mock_ptp_time(ctx_, current_tai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + + ASSERT_EQ(ut_txa_prepare_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, 0, 1), 0); + EXPECT_EQ(ut_txa_step_frame_tasklet(ctx_), 0); + EXPECT_EQ(ut_txa_queued_packets(ctx_), 0u); + EXPECT_EQ(ut_txa_cur_epochs(ctx_), target_epoch); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc + target_tai - current_tai); + EXPECT_EQ(ut_txa_step_frame_tasklet(ctx_), 1); + ASSERT_EQ(ut_txa_pop_packet_tsc(ctx_, &packet_tsc), 0); + EXPECT_EQ(packet_tsc, kCurrentTsc + target_tai - current_tai); + EXPECT_EQ(ut_txa_notify_frame_done_epoch(ctx_), target_epoch); + EXPECT_EQ(ut_txa_notify_frame_done_timestamp(ctx_), target_tai); + EXPECT_EQ(ut_txa_notify_frame_done_tfmt(ctx_), ST10_TIMESTAMP_FMT_TAI); + ExpectNoPacingStats(); +} + +TEST_F(St40TxPacingTest, ExactTargetOneNanosecondBelowOneSecondWaits) { + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_exact_user_pacing(ctx_, true); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + ASSERT_EQ(ut_txa_prepare_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_TAI, + kCurrentTai + kNanosecondsPerSecond - 1, 1), + 0); + + EXPECT_EQ(ut_txa_step_frame_tasklet(ctx_), 0); + EXPECT_EQ(ut_txa_queued_packets(ctx_), 0u); + EXPECT_EQ(ut_txa_notify_frame_done_calls(ctx_), 0); +} + +TEST_F(St40TxPacingTest, ExactTargetAtOneSecondWaits) { + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_exact_user_pacing(ctx_, true); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + ASSERT_EQ(ut_txa_prepare_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_TAI, + kCurrentTai + kNanosecondsPerSecond, 1), + 0); + + EXPECT_EQ(ut_txa_step_frame_tasklet(ctx_), 0); + EXPECT_EQ(ut_txa_queued_packets(ctx_), 0u); + EXPECT_EQ(ut_txa_notify_frame_done_calls(ctx_), 0); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc + kNanosecondsPerSecond); + EXPECT_EQ(ut_txa_step_frame_tasklet(ctx_), 1); + EXPECT_EQ(ut_txa_queued_packets(ctx_), 1u); +} + +TEST_F(St40TxPacingTest, ExactTargetBeyondOneSecondFallsBackWithoutWaiting) { + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_exact_user_pacing(ctx_, true); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + ASSERT_EQ(ut_txa_prepare_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_TAI, + kCurrentTai + kNanosecondsPerSecond + 1, 1), + 0); + + EXPECT_EQ(ut_txa_step_frame_tasklet(ctx_), 1); + EXPECT_EQ(ut_txa_queued_packets(ctx_), 1u); + EXPECT_EQ(ut_txa_notify_frame_done_timestamp(ctx_), kCurrentTai); + EXPECT_EQ(ut_txa_notify_frame_done_epoch(ctx_), kCurrentEpoch); + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 1u); +} + +TEST_F(St40TxPacingTest, ForwardAlignedTargetUpdatesCompletionMetadata) { + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + uint64_t packet_tsc = 0; + + ASSERT_EQ( + ut_txa_run_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_TAI, kTargetTai, &packet_tsc), 0); + EXPECT_EQ(ut_txa_notify_frame_done_epoch(ctx_), kAlignedTargetTai / kFramePeriodNs); + EXPECT_EQ(ut_txa_notify_frame_done_timestamp(ctx_), kAlignedTargetTai); + EXPECT_EQ(ut_txa_notify_frame_done_tfmt(ctx_), ST10_TIMESTAMP_FMT_TAI); + EXPECT_EQ(ut_txa_notify_frame_done_rtp_timestamp(ctx_), + kAlignedTargetTai * kMediaClockRate / kNanosecondsPerSecond); +} + +TEST_F(St40TxPacingTest, ForwardExactTargetUpdatesCompletionMetadata) { + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_exact_user_pacing(ctx_, true); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + uint64_t packet_tsc = 0; + + ASSERT_EQ( + ut_txa_run_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_TAI, kTargetTai, &packet_tsc), 0); + EXPECT_EQ(ut_txa_notify_frame_done_epoch(ctx_), + (kTargetTai + kFramePeriodNs / 2) / kFramePeriodNs); + EXPECT_EQ(ut_txa_notify_frame_done_timestamp(ctx_), kTargetTai); + EXPECT_EQ(ut_txa_notify_frame_done_tfmt(ctx_), ST10_TIMESTAMP_FMT_TAI); + EXPECT_EQ(ut_txa_notify_frame_done_rtp_timestamp(ctx_), + kTargetTai * kMediaClockRate / kNanosecondsPerSecond); +} + +TEST_F(St40TxPacingTest, BackwardExactTargetFallsBackCompletionMetadata) { + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_exact_user_pacing(ctx_, true); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + uint64_t packet_tsc = 0; + + ASSERT_EQ(ut_txa_run_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_TAI, kCurrentTai - 1, + &packet_tsc), + 0); + EXPECT_EQ(ut_txa_notify_frame_done_epoch(ctx_), kCurrentEpoch); + EXPECT_EQ(ut_txa_notify_frame_done_timestamp(ctx_), kCurrentTai); + EXPECT_EQ(ut_txa_notify_frame_done_rtp_timestamp(ctx_), + kCurrentTai * kMediaClockRate / kNanosecondsPerSecond); + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 1u); +} + +TEST_F(St40TxPacingTest, OversizedMediaClockUserPacingCountsOneError) { + ut_txa_set_user_pacing(ctx_, true); + + EXPECT_EQ(ut_txa_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, + (uint64_t)UINT32_MAX + 1), + 0u); + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 1u); +} + +TEST_F(St40TxPacingTest, OversizedMediaClockExactPacingCountsOneError) { + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_exact_user_pacing(ctx_, true); + + EXPECT_EQ(ut_txa_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, + (uint64_t)UINT32_MAX + 1), + 0u); + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 1u); +} + +TEST_F(St40TxPacingTest, ExactTimestampNearUint64MaxSaturatesTscTarget) { + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_exact_user_pacing(ctx_, true); + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, 2 * kCurrentTai); + + ASSERT_EQ(ut_txa_sync_pacing(ctx_, UINT64_MAX), 0); + + EXPECT_EQ((uint64_t)ut_txa_tsc_time_cursor(ctx_), UINT64_MAX); + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txa_stat_epoch_mismatch(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_onward(ctx_), 0u); +} + +TEST_F(St40TxPacingTest, AlignedTimestampNearUint64MaxDoesNotSendImmediately) { + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + + ASSERT_EQ(ut_txa_sync_pacing(ctx_, UINT64_MAX), 0); + + EXPECT_GT(ut_txa_tsc_time_cursor(ctx_), kCurrentTsc); + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txa_stat_epoch_mismatch(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_onward(ctx_), 0u); +} + +TEST_F(St40TxPacingTest, ExactTimestampAtInt64MaxRemainsInFuture) { + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_exact_user_pacing(ctx_, true); + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + + ASSERT_EQ(ut_txa_sync_pacing(ctx_, INT64_MAX), 0); + + EXPECT_GT(ut_txa_tsc_time_cursor(ctx_), kCurrentTsc); + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txa_stat_epoch_mismatch(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_onward(ctx_), 0u); +} + +TEST_F(St40TxPacingTest, AlignedTimestampAtInt64MaxRemainsInFuture) { + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + + ASSERT_EQ(ut_txa_sync_pacing(ctx_, INT64_MAX), 0); + + EXPECT_GT(ut_txa_tsc_time_cursor(ctx_), kCurrentTsc); + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txa_stat_epoch_mismatch(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_drop(ctx_), 0u); + EXPECT_EQ(ut_txa_stat_epoch_onward(ctx_), 0u); +} + +TEST_F(St40TxPacingTest, ExactZeroTimestampIsFlaggedAndFallsBackToEpochPacing) { + ut_txa_set_user_pacing(ctx_, true); + ut_txa_set_exact_user_pacing(ctx_, true); + ut_txa_set_cur_epochs(ctx_, kInitialEpoch); + ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); + ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); + + uint64_t required_tai = ut_txa_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_TAI, 0); + ASSERT_EQ(required_tai, 0u); + ASSERT_EQ(ut_txa_sync_pacing(ctx_, required_tai), 0); + + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 1u); + EXPECT_EQ(ut_txa_cur_epochs(ctx_), kCurrentEpoch); + EXPECT_EQ(ut_txa_ptp_time_cursor(ctx_), kCurrentTai); + EXPECT_EQ(ut_txa_tsc_time_cursor(ctx_), kCurrentTsc); +} diff --git a/tests/unit/session/st40_tx_harness.c b/tests/unit/session/st40_tx_harness.c new file mode 100644 index 000000000..03844c154 --- /dev/null +++ b/tests/unit/session/st40_tx_harness.c @@ -0,0 +1,340 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + */ + +#include + +#undef MTL_HAS_USDT +#include "common/ut_common.h" +#include "mt_main.h" +#include "st2110/st_tx_ancillary_session.h" + +struct ut_txa_ctx { + struct mtl_main_impl impl; + struct st_tx_ancillary_sessions_mgr mgr; + struct st_tx_ancillary_session_impl session; + struct st_frame_trans frame; + struct st40_frame frame_data; + uint64_t mock_ptp_ns; + uint64_t mock_tsc_ns; + enum st10_timestamp_fmt app_tfmt; + uint64_t app_timestamp; + int get_next_frame_calls; + int notify_frame_done_calls; + uint16_t notify_frame_done_idx; + struct st40_tx_frame_meta notify_frame_done_meta; + enum st40_tx_frame_status frame_status; + int frame_refcnt; + uint32_t packet_len; + int notify_late_calls; + uint64_t notify_late_last_delta; +}; + +#include "session/st40_tx_harness.h" + +static uint64_t ut_txa_ptp_time_fn(struct mtl_main_impl* impl, enum mtl_port port) { + (void)port; + struct ut_txa_ctx* ctx = (struct ut_txa_ctx*)impl; + return ctx->mock_ptp_ns; +} + +static uint64_t ut_txa_tsc_time_fn(struct mtl_main_impl* impl) { + struct ut_txa_ctx* ctx = (struct ut_txa_ctx*)impl; + return ctx->mock_tsc_ns; +} + +static int ut_txa_notify_frame_late(void* priv, uint64_t epoch_skipped) { + struct ut_txa_ctx* ctx = priv; + ctx->notify_late_calls++; + ctx->notify_late_last_delta = epoch_skipped; + return 0; +} + +static int ut_txa_get_next_frame(void* priv, uint16_t* next_frame_idx, + struct st40_tx_frame_meta* meta) { + struct ut_txa_ctx* ctx = priv; + if (ctx->get_next_frame_calls) return -EIO; + ctx->get_next_frame_calls++; + *next_frame_idx = 0; + meta->tfmt = ctx->app_tfmt; + meta->timestamp = ctx->app_timestamp; + return 0; +} + +static int ut_txa_notify_frame_done(void* priv, uint16_t frame_idx, + struct st40_tx_frame_meta* meta) { + struct ut_txa_ctx* ctx = priv; + ctx->notify_frame_done_calls++; + ctx->notify_frame_done_idx = frame_idx; + ctx->notify_frame_done_meta = *meta; + return 0; +} + +#define mt_get_tsc ut_txa_tsc_time_fn +#include "st2110/st_tx_ancillary_session.c" +#undef mt_get_tsc + +int ut_txa_init(void) { + return ut_eal_init(); +} + +ut_txa_ctx* ut_txa_create(void) { + ut_txa_ctx* ctx = calloc(1, sizeof(*ctx)); + if (!ctx) return NULL; + + ctx->impl.type = MT_HANDLE_MAIN; + ctx->impl.tsc_hz = rte_get_tsc_hz(); + ctx->impl.inf[MTL_PORT_P].ptp_get_time_fn = ut_txa_ptp_time_fn; + ctx->mgr.parent = &ctx->impl; + ctx->mgr.max_idx = 1; + rte_spinlock_init(&ctx->mgr.mutex[0]); + ctx->session.mgr = &ctx->mgr; + ctx->session.pacing.frame_time = NS_PER_MS; + ctx->session.pacing.max_onward_epochs = 3; + ctx->session.fps_tm.sampling_clock_rate = ST10_VIDEO_SAMPLING_RATE_90K; + ctx->session.ops.get_next_frame = ut_txa_get_next_frame; + ctx->session.ops.notify_frame_done = ut_txa_notify_frame_done; + ctx->session.ops.notify_frame_late = ut_txa_notify_frame_late; + ctx->session.ops.priv = ctx; + ctx->mgr.sessions[0] = &ctx->session; + return ctx; +} + +void ut_txa_destroy(ut_txa_ctx* ctx) { + ut_txa_cleanup_frame_tasklet(ctx); + free(ctx); +} + +void ut_txa_set_cur_epochs(ut_txa_ctx* ctx, uint64_t cur_epochs) { + ctx->session.pacing.cur_epochs = cur_epochs; +} + +void ut_txa_set_user_pacing(ut_txa_ctx* ctx, bool enable) { + if (enable) + ctx->session.ops.flags |= ST40_TX_FLAG_USER_PACING; + else + ctx->session.ops.flags &= ~ST40_TX_FLAG_USER_PACING; +} + +void ut_txa_set_exact_user_pacing(ut_txa_ctx* ctx, bool enable) { + if (enable) + ctx->session.ops.flags |= ST40_TX_FLAG_EXACT_USER_PACING; + else + ctx->session.ops.flags &= ~ST40_TX_FLAG_EXACT_USER_PACING; +} + +void ut_txa_set_mock_ptp_time(ut_txa_ctx* ctx, uint64_t ptp_ns) { + ctx->mock_ptp_ns = ptp_ns; +} + +void ut_txa_set_mock_tsc_time(ut_txa_ctx* ctx, uint64_t tsc_ns) { + ctx->mock_tsc_ns = tsc_ns; +} + +uint64_t ut_txa_calc_epoch(ut_txa_ctx* ctx, uint64_t cur_tai, uint64_t required_tai) { + return tx_ancillary_calc_epoch(&ctx->session, cur_tai, required_tai); +} + +uint64_t ut_txa_pacing_required_tai(ut_txa_ctx* ctx, enum st10_timestamp_fmt tfmt, + uint64_t timestamp) { + return tx_ancillary_pacing_required_tai(&ctx->session, tfmt, timestamp); +} + +int ut_txa_sync_pacing(ut_txa_ctx* ctx, uint64_t required_tai) { + return tx_ancillary_session_sync_pacing(&ctx->impl, &ctx->session, required_tai); +} + +int ut_txa_prepare_frame_tasklet(ut_txa_ctx* ctx, enum st10_timestamp_fmt tfmt, + uint64_t timestamp, unsigned int packets) { + static unsigned int test_idx; + struct st_tx_ancillary_session_impl* s = &ctx->session; + char pool_name[RTE_MEMPOOL_NAMESIZE]; + char ring_name[RTE_RING_NAMESIZE]; + + if (!packets || packets > 2) return -EINVAL; + ut_txa_cleanup_frame_tasklet(ctx); + + snprintf(pool_name, sizeof(pool_name), "ut_txa_pool_%u", test_idx); + snprintf(ring_name, sizeof(ring_name), "ut_txa_ring_%u", test_idx++); + s->mbuf_mempool_hdr[MTL_SESSION_PORT_P] = + rte_pktmbuf_pool_create(pool_name, 32, 0, sizeof(struct mt_muf_priv_data), + RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id()); + ctx->mgr.ring[MTL_PORT_P] = ut_ring_create(ring_name, 32); + if (!s->mbuf_mempool_hdr[MTL_SESSION_PORT_P] || !ctx->mgr.ring[MTL_PORT_P]) { + ut_txa_cleanup_frame_tasklet(ctx); + return -ENOMEM; + } + + memset(&ctx->frame, 0, sizeof(ctx->frame)); + memset(&ctx->frame_data, 0, sizeof(ctx->frame_data)); + ctx->frame.addr = &ctx->frame_data; + ctx->frame_data.meta_num = packets == 2 ? 2 : 0; + s->st40_frames = &ctx->frame; + s->st40_frames_cnt = 1; + s->st40_frame_stat = ST40_TX_STAT_WAIT_FRAME; + s->calculate_time_cursor = true; + s->max_pkt_len = 1200; + s->tx_no_chain = true; + s->split_payload = packets == 2; + s->ops.num_port = 1; + s->ops.type = ST40_TYPE_FRAME_LEVEL; + ctx->app_tfmt = tfmt; + ctx->app_timestamp = timestamp; + ctx->get_next_frame_calls = 0; + ctx->notify_frame_done_calls = 0; + memset(&ctx->notify_frame_done_meta, 0, sizeof(ctx->notify_frame_done_meta)); + return 0; +} + +int ut_txa_step_frame_tasklet(ut_txa_ctx* ctx) { + return tx_ancillary_sessions_tasklet_handler(&ctx->mgr); +} + +unsigned int ut_txa_queued_packets(const ut_txa_ctx* ctx) { + if (!ctx->mgr.ring[MTL_PORT_P]) return 0; + return rte_ring_count(ctx->mgr.ring[MTL_PORT_P]); +} + +int ut_txa_pop_packet_tsc(ut_txa_ctx* ctx, uint64_t* packet_tsc) { + struct rte_mbuf* packet = NULL; + + if (!ctx->mgr.ring[MTL_PORT_P] || + rte_ring_sc_dequeue(ctx->mgr.ring[MTL_PORT_P], (void**)&packet) < 0) + return -ENOENT; + *packet_tsc = st_tx_mbuf_get_tsc(packet); + rte_pktmbuf_free(packet); + return 0; +} + +void ut_txa_cleanup_frame_tasklet(ut_txa_ctx* ctx) { + struct st_tx_ancillary_session_impl* s; + + if (!ctx) return; + s = &ctx->session; + if (ctx->mgr.ring[MTL_PORT_P]) { + ut_ring_drain(ctx->mgr.ring[MTL_PORT_P]); + rte_ring_free(ctx->mgr.ring[MTL_PORT_P]); + ctx->mgr.ring[MTL_PORT_P] = NULL; + } + if (s->mbuf_mempool_hdr[MTL_SESSION_PORT_P]) { + rte_mempool_free(s->mbuf_mempool_hdr[MTL_SESSION_PORT_P]); + s->mbuf_mempool_hdr[MTL_SESSION_PORT_P] = NULL; + } + s->st40_frames = NULL; +} + +int ut_txa_run_frame_tasklet(ut_txa_ctx* ctx, enum st10_timestamp_fmt tfmt, + uint64_t timestamp, uint64_t* packet_tsc) { + int ret = ut_txa_prepare_frame_tasklet(ctx, tfmt, timestamp, 1); + if (ret < 0) return ret; + + ut_txa_step_frame_tasklet(ctx); + ctx->mock_tsc_ns = (uint64_t)ctx->session.pacing.tsc_time_cursor; + ut_txa_step_frame_tasklet(ctx); + ctx->frame_status = ctx->session.st40_frame_stat; + ctx->frame_refcnt = rte_atomic32_read(&ctx->frame.refcnt); + ret = ut_txa_pop_packet_tsc(ctx, packet_tsc); + ctx->packet_len = ctx->session.port_user_stats.common.port[MTL_SESSION_PORT_P].bytes; + ut_txa_cleanup_frame_tasklet(ctx); + return ret; +} + +uint64_t ut_txa_cur_epochs(const ut_txa_ctx* ctx) { + return ctx->session.pacing.cur_epochs; +} + +uint64_t ut_txa_ptp_time_cursor(const ut_txa_ctx* ctx) { + return (uint64_t)ctx->session.pacing.ptp_time_cursor; +} + +uint64_t ut_txa_tsc_time_cursor(const ut_txa_ctx* ctx) { + return (uint64_t)ctx->session.pacing.tsc_time_cursor; +} + +uint64_t ut_txa_stat_epoch_onward(const ut_txa_ctx* ctx) { + return ctx->session.port_user_stats.common.stat_epoch_onward; +} + +uint64_t ut_txa_stat_epoch_drop(const ut_txa_ctx* ctx) { + return ctx->session.port_user_stats.common.stat_epoch_drop; +} + +uint64_t ut_txa_stat_error_user_timestamp(const ut_txa_ctx* ctx) { + return ctx->session.port_user_stats.common.stat_error_user_timestamp; +} + +uint64_t ut_txa_stat_epoch_mismatch(const ut_txa_ctx* ctx) { + return ctx->session.port_user_stats.common.stat_epoch_mismatch; +} + +int ut_txa_notify_late_calls(const ut_txa_ctx* ctx) { + return ctx->notify_late_calls; +} + +uint64_t ut_txa_notify_late_last_delta(const ut_txa_ctx* ctx) { + return ctx->notify_late_last_delta; +} + +int ut_txa_get_next_frame_calls(const ut_txa_ctx* ctx) { + return ctx->get_next_frame_calls; +} + +int ut_txa_notify_frame_done_calls(const ut_txa_ctx* ctx) { + return ctx->notify_frame_done_calls; +} + +uint16_t ut_txa_notify_frame_done_idx(const ut_txa_ctx* ctx) { + return ctx->notify_frame_done_idx; +} + +uint64_t ut_txa_notify_frame_done_epoch(const ut_txa_ctx* ctx) { + return ctx->notify_frame_done_meta.epoch; +} + +uint64_t ut_txa_notify_frame_done_timestamp(const ut_txa_ctx* ctx) { + return ctx->notify_frame_done_meta.timestamp; +} + +enum st10_timestamp_fmt ut_txa_notify_frame_done_tfmt(const ut_txa_ctx* ctx) { + return ctx->notify_frame_done_meta.tfmt; +} + +uint32_t ut_txa_notify_frame_done_rtp_timestamp(const ut_txa_ctx* ctx) { + return ctx->notify_frame_done_meta.rtp_timestamp; +} + +bool ut_txa_frame_is_waiting(const ut_txa_ctx* ctx) { + return ctx->frame_status == ST40_TX_STAT_WAIT_FRAME; +} + +int ut_txa_frame_refcnt(const ut_txa_ctx* ctx) { + return ctx->frame_refcnt; +} + +uint64_t ut_txa_stat_port_build(const ut_txa_ctx* ctx) { + return ctx->session.port_user_stats.common.port[MTL_SESSION_PORT_P].build; +} + +uint64_t ut_txa_stat_port_packets(const ut_txa_ctx* ctx) { + return ctx->session.port_user_stats.common.port[MTL_SESSION_PORT_P].packets; +} + +uint64_t ut_txa_stat_port_bytes(const ut_txa_ctx* ctx) { + return ctx->session.port_user_stats.common.port[MTL_SESSION_PORT_P].bytes; +} + +uint32_t ut_txa_packet_len(const ut_txa_ctx* ctx) { + return ctx->packet_len; +} + +uint64_t ut_txa_stat_port_frames(const ut_txa_ctx* ctx) { + return ctx->session.port_user_stats.common.port[MTL_SESSION_PORT_P].frames; +} + +uint64_t ut_txa_stat_recoverable_error(const ut_txa_ctx* ctx) { + return ctx->session.port_user_stats.common.stat_recoverable_error; +} + +uint64_t ut_txa_stat_unrecoverable_error(const ut_txa_ctx* ctx) { + return ctx->session.port_user_stats.common.stat_unrecoverable_error; +} diff --git a/tests/unit/session/st40_tx_harness.h b/tests/unit/session/st40_tx_harness.h new file mode 100644 index 000000000..f3e303d3c --- /dev/null +++ b/tests/unit/session/st40_tx_harness.h @@ -0,0 +1,69 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + */ + +#ifndef _ST40_TX_SESSION_HARNESS_H_ +#define _ST40_TX_SESSION_HARNESS_H_ + +#include +#include + +#include "st_api.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct ut_txa_ctx ut_txa_ctx; + +int ut_txa_init(void); +ut_txa_ctx* ut_txa_create(void); +void ut_txa_destroy(ut_txa_ctx* ctx); +void ut_txa_set_cur_epochs(ut_txa_ctx* ctx, uint64_t cur_epochs); +void ut_txa_set_user_pacing(ut_txa_ctx* ctx, bool enable); +void ut_txa_set_exact_user_pacing(ut_txa_ctx* ctx, bool enable); +void ut_txa_set_mock_ptp_time(ut_txa_ctx* ctx, uint64_t ptp_ns); +void ut_txa_set_mock_tsc_time(ut_txa_ctx* ctx, uint64_t tsc_ns); +uint64_t ut_txa_calc_epoch(ut_txa_ctx* ctx, uint64_t cur_tai, uint64_t required_tai); +uint64_t ut_txa_pacing_required_tai(ut_txa_ctx* ctx, enum st10_timestamp_fmt tfmt, + uint64_t timestamp); +int ut_txa_sync_pacing(ut_txa_ctx* ctx, uint64_t required_tai); +int ut_txa_prepare_frame_tasklet(ut_txa_ctx* ctx, enum st10_timestamp_fmt tfmt, + uint64_t timestamp, unsigned int packets); +int ut_txa_step_frame_tasklet(ut_txa_ctx* ctx); +unsigned int ut_txa_queued_packets(const ut_txa_ctx* ctx); +int ut_txa_pop_packet_tsc(ut_txa_ctx* ctx, uint64_t* packet_tsc); +void ut_txa_cleanup_frame_tasklet(ut_txa_ctx* ctx); +int ut_txa_run_frame_tasklet(ut_txa_ctx* ctx, enum st10_timestamp_fmt tfmt, + uint64_t timestamp, uint64_t* packet_tsc); +uint64_t ut_txa_cur_epochs(const ut_txa_ctx* ctx); +uint64_t ut_txa_ptp_time_cursor(const ut_txa_ctx* ctx); +uint64_t ut_txa_tsc_time_cursor(const ut_txa_ctx* ctx); +uint64_t ut_txa_stat_epoch_onward(const ut_txa_ctx* ctx); +uint64_t ut_txa_stat_epoch_drop(const ut_txa_ctx* ctx); +uint64_t ut_txa_stat_error_user_timestamp(const ut_txa_ctx* ctx); +uint64_t ut_txa_stat_epoch_mismatch(const ut_txa_ctx* ctx); +int ut_txa_notify_late_calls(const ut_txa_ctx* ctx); +uint64_t ut_txa_notify_late_last_delta(const ut_txa_ctx* ctx); +int ut_txa_get_next_frame_calls(const ut_txa_ctx* ctx); +int ut_txa_notify_frame_done_calls(const ut_txa_ctx* ctx); +uint16_t ut_txa_notify_frame_done_idx(const ut_txa_ctx* ctx); +uint64_t ut_txa_notify_frame_done_epoch(const ut_txa_ctx* ctx); +uint64_t ut_txa_notify_frame_done_timestamp(const ut_txa_ctx* ctx); +enum st10_timestamp_fmt ut_txa_notify_frame_done_tfmt(const ut_txa_ctx* ctx); +uint32_t ut_txa_notify_frame_done_rtp_timestamp(const ut_txa_ctx* ctx); +bool ut_txa_frame_is_waiting(const ut_txa_ctx* ctx); +int ut_txa_frame_refcnt(const ut_txa_ctx* ctx); +uint64_t ut_txa_stat_port_build(const ut_txa_ctx* ctx); +uint64_t ut_txa_stat_port_packets(const ut_txa_ctx* ctx); +uint64_t ut_txa_stat_port_bytes(const ut_txa_ctx* ctx); +uint32_t ut_txa_packet_len(const ut_txa_ctx* ctx); +uint64_t ut_txa_stat_port_frames(const ut_txa_ctx* ctx); +uint64_t ut_txa_stat_recoverable_error(const ut_txa_ctx* ctx); +uint64_t ut_txa_stat_unrecoverable_error(const ut_txa_ctx* ctx); + +#ifdef __cplusplus +} +#endif + +#endif /* _ST40_TX_SESSION_HARNESS_H_ */ From 1777dcc510676538dfcc90a22b9a99ba73611e0a Mon Sep 17 00:00:00 2001 From: "Kasiewicz, Marek" Date: Tue, 14 Jul 2026 11:42:45 +0000 Subject: [PATCH 14/19] Fix: Reject media clock timestamps for ST40 user pacing ST40_TX_FLAG_USER_PACING accepted ST10_TIMESTAMP_FMT_MEDIA_CLK and converted it to TAI by reconstructing the wrap era from the current media clock, an ambiguous operation whenever scheduling jitter shifts the target across a 32-bit wrap boundary. ST20 already rejects media clock for pacing; ST40 now does the same, falling back to the default epoch-based pacing and counting a user-timestamp error. Media clock is still accepted for ST40_TX_FLAG_USER_TIMESTAMP, which only affects the reported RTP timestamp and never TAI conversion. st10_media_clk_to_ns_u64() is no longer needed and is removed along with its declaration. Signed-off-by: Kasiewicz, Marek --- lib/src/st2110/st_fmt.c | 6 +- lib/src/st2110/st_fmt.h | 2 - lib/src/st2110/st_tx_ancillary_session.c | 41 ++--- tests/unit/session/st40_tx/pacing_test.cpp | 177 +-------------------- 4 files changed, 19 insertions(+), 207 deletions(-) diff --git a/lib/src/st2110/st_fmt.c b/lib/src/st2110/st_fmt.c index 2a8d7b245..eb7a228b4 100644 --- a/lib/src/st2110/st_fmt.c +++ b/lib/src/st2110/st_fmt.c @@ -928,7 +928,7 @@ uint32_t st10_tai_to_media_clk(uint64_t tai_ns, uint32_t sampling_rate) { return (uint32_t)st_muldiv_u64_round_closest(tai_ns, sampling_rate, NS_PER_S); } -uint64_t st10_media_clk_to_ns_u64(uint64_t media_ts, uint32_t sampling_rate) { +uint64_t st10_media_clk_to_ns(uint32_t media_ts, uint32_t sampling_rate) { if (!sampling_rate) { err("%s, invalid sampling rate\n", __func__); return 0; @@ -937,10 +937,6 @@ uint64_t st10_media_clk_to_ns_u64(uint64_t media_ts, uint32_t sampling_rate) { return st_muldiv_u64_round_closest(media_ts, NS_PER_S, sampling_rate); } -uint64_t st10_media_clk_to_ns(uint32_t media_ts, uint32_t sampling_rate) { - return st10_media_clk_to_ns_u64(media_ts, sampling_rate); -} - int st_draw_logo(struct st_frame* frame, struct st_frame* logo, uint32_t x, uint32_t y) { if (frame->fmt != logo->fmt) { err("%s, mismatch fmt %d %d\n", __func__, frame->fmt, logo->fmt); diff --git a/lib/src/st2110/st_fmt.h b/lib/src/st2110/st_fmt.h index b1878ead3..2f846a355 100644 --- a/lib/src/st2110/st_fmt.h +++ b/lib/src/st2110/st_fmt.h @@ -39,8 +39,6 @@ const char* st_tx_pacing_way_name(enum st21_tx_pacing_way way); int st_get_fps_timing(enum st_fps fps, struct st_fps_timing* fps_tm); -uint64_t st10_media_clk_to_ns_u64(uint64_t media_ts, uint32_t sampling_rate); - enum st_frame_sampling st_frame_fmt_get_sampling(enum st_frame_fmt fmt); int st22_rtp_bandwidth_bps(uint32_t total_pkts, uint16_t pkt_size, enum st_fps fps, diff --git a/lib/src/st2110/st_tx_ancillary_session.c b/lib/src/st2110/st_tx_ancillary_session.c index b67245fee..198ed6696 100644 --- a/lib/src/st2110/st_tx_ancillary_session.c +++ b/lib/src/st2110/st_tx_ancillary_session.c @@ -300,10 +300,9 @@ static uint64_t tx_ancillary_pacing_required_tai(struct st_tx_ancillary_session_ enum st10_timestamp_fmt tfmt, uint64_t timestamp) { uint64_t required_tai = 0; - uint64_t cur_tai; if (!(s->ops.flags & ST40_TX_FLAG_USER_PACING)) return 0; - if (!timestamp && tfmt != ST10_TIMESTAMP_FMT_MEDIA_CLK) { + if (!timestamp) { if (s->ops.flags & ST40_TX_FLAG_EXACT_USER_PACING) { s->port_user_stats.common.stat_error_user_timestamp++; err("%s(%d), EXACT_USER_PACING requires non-zero timestamp\n", __func__, s->idx); @@ -312,39 +311,19 @@ static uint64_t tx_ancillary_pacing_required_tai(struct st_tx_ancillary_session_ } if (tfmt == ST10_TIMESTAMP_FMT_MEDIA_CLK) { - uint64_t current_ticks; - uint64_t target_ticks; - uint32_t forward; + s->port_user_stats.common.stat_error_user_timestamp++; + err("%s(%d), Media clock can't be used for user-controlled pacing\n", __func__, + s->idx); + return 0; /* invalid timestamp, fallback to default pacing */ + } + required_tai = timestamp; - if (timestamp > UINT32_MAX) { - err("%s(%d), invalid timestamp %" PRIu64 "\n", __func__, s->idx, timestamp); + if (s->ops.flags & ST40_TX_FLAG_EXACT_USER_PACING) { + uint64_t cur_tai = mt_get_ptp_time(s->mgr->parent, MTL_PORT_P); + if (required_tai < cur_tai || required_tai - cur_tai > NS_PER_S) { s->port_user_stats.common.stat_error_user_timestamp++; return 0; } - cur_tai = mt_get_ptp_time(s->mgr->parent, MTL_PORT_P); - current_ticks = (cur_tai / NS_PER_S) * ST10_VIDEO_SAMPLING_RATE_90K; - current_ticks += - st10_tai_to_media_clk(cur_tai % NS_PER_S, ST10_VIDEO_SAMPLING_RATE_90K); - - forward = (uint32_t)timestamp - (uint32_t)current_ticks; - if (forward <= INT32_MAX) { - target_ticks = current_ticks + forward; - } else { - uint64_t backward = (uint64_t)UINT32_MAX - forward + 1; - target_ticks = (current_ticks >= backward) ? current_ticks - backward - : current_ticks + forward; - } - - required_tai = st10_media_clk_to_ns_u64(target_ticks, ST10_VIDEO_SAMPLING_RATE_90K); - } else { - required_tai = timestamp; - cur_tai = mt_get_ptp_time(s->mgr->parent, MTL_PORT_P); - } - - if ((s->ops.flags & ST40_TX_FLAG_EXACT_USER_PACING) && - (required_tai < cur_tai || required_tai - cur_tai > NS_PER_S)) { - s->port_user_stats.common.stat_error_user_timestamp++; - return 0; } return required_tai; diff --git a/tests/unit/session/st40_tx/pacing_test.cpp b/tests/unit/session/st40_tx/pacing_test.cpp index 543eebccf..de1f447aa 100644 --- a/tests/unit/session/st40_tx/pacing_test.cpp +++ b/tests/unit/session/st40_tx/pacing_test.cpp @@ -20,19 +20,7 @@ constexpr uint64_t kTargetTai = kCurrentTai + kFramePeriodNs / 2; constexpr uint64_t kAlignedTargetTai = kCurrentTai + kFramePeriodNs; constexpr uint64_t kPastTai = kCurrentTai - kFramePeriodNs / 2; constexpr uint64_t kMediaClockRate = ST10_VIDEO_SAMPLING_RATE_90K; -constexpr uint64_t kMediaClockModulus = static_cast(UINT32_MAX) + 1; constexpr uint64_t kStaleEpoch = 1000; -constexpr uint64_t kWrapTestMediaClockEra = 30000; -constexpr uint64_t kZeroTimestampMediaClockEra = 37300; - -constexpr uint64_t MediaClockToNanoseconds(uint64_t ticks) { - return (static_cast<__uint128_t>(ticks) * kNanosecondsPerSecond + kMediaClockRate / 2) / - kMediaClockRate; -} - -constexpr uint64_t AlignToFrame(uint64_t tai) { - return ((tai + kFramePeriodNs / 2) / kFramePeriodNs) * kFramePeriodNs; -} } // namespace class St40TxPacingTest : public ::testing::Test { @@ -406,7 +394,7 @@ TEST_F(St40TxPacingTest, RepeatedEpochMismatchAccumulates) { EXPECT_EQ(ut_txa_notify_late_calls(ctx_), 0); } -TEST_F(St40TxPacingTest, MediaClockUserPacingAlignsConvertedTimestamp) { +TEST_F(St40TxPacingTest, MediaClockUserPacingFallsBackToDefaultPacing) { ut_txa_set_user_pacing(ctx_, true); constexpr uint64_t media_clock_timestamp = 945; ut_txa_set_cur_epochs(ctx_, kInitialEpoch); @@ -415,152 +403,22 @@ TEST_F(St40TxPacingTest, MediaClockUserPacingAlignsConvertedTimestamp) { uint64_t required_tai = ut_txa_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, media_clock_timestamp); - ASSERT_EQ(required_tai, MediaClockToNanoseconds(media_clock_timestamp)); + ASSERT_EQ(required_tai, 0u); ASSERT_EQ(ut_txa_sync_pacing(ctx_, required_tai), 0); - EXPECT_EQ(ut_txa_ptp_time_cursor(ctx_), kAlignedTargetTai); + EXPECT_EQ(ut_txa_ptp_time_cursor(ctx_), kCurrentTai); + EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 1u); } -TEST_F(St40TxPacingTest, MediaClockExactUserPacingUsesConvertedTimestampVerbatim) { +TEST_F(St40TxPacingTest, ExactUnsupportedMediaClockCountsOneError) { ut_txa_set_user_pacing(ctx_, true); ut_txa_set_exact_user_pacing(ctx_, true); constexpr uint64_t media_clock_timestamp = 945; - ut_txa_set_cur_epochs(ctx_, kInitialEpoch); - ut_txa_set_mock_ptp_time(ctx_, kCurrentTai); - ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); - - uint64_t required_tai = ut_txa_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, - media_clock_timestamp); - ASSERT_EQ(required_tai, MediaClockToNanoseconds(media_clock_timestamp)); - ASSERT_EQ(ut_txa_sync_pacing(ctx_, required_tai), 0); - - EXPECT_EQ(ut_txa_ptp_time_cursor(ctx_), MediaClockToNanoseconds(media_clock_timestamp)); -} - -TEST_F(St40TxPacingTest, MediaClockExactTaskletUnwrapsFutureAcrossUint32Wrap) { - constexpr uint64_t media_clock_era = kWrapTestMediaClockEra; - constexpr uint64_t current_ticks = - media_clock_era * kMediaClockModulus + UINT32_MAX - 9; - constexpr uint64_t target_ticks = (media_clock_era + 1) * kMediaClockModulus + 5; - constexpr uint64_t current_tai = MediaClockToNanoseconds(current_ticks); - constexpr uint64_t target_tai = MediaClockToNanoseconds(target_ticks); - ut_txa_set_user_pacing(ctx_, true); - ut_txa_set_exact_user_pacing(ctx_, true); - ut_txa_set_cur_epochs(ctx_, current_tai / kFramePeriodNs); - ut_txa_set_mock_ptp_time(ctx_, current_tai); - ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); - uint64_t packet_tsc = 0; - - ASSERT_EQ(ut_txa_run_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, 5, &packet_tsc), - 0); - - EXPECT_EQ(packet_tsc, kCurrentTsc + target_tai - current_tai); - EXPECT_EQ(ut_txa_ptp_time_cursor(ctx_), target_tai); - ExpectNoPacingStats(); -} - -TEST_F(St40TxPacingTest, MediaClockAlignedTaskletUnwrapsFutureAcrossUint32Wrap) { - constexpr uint64_t media_clock_era = kWrapTestMediaClockEra; - constexpr uint64_t current_ticks = - media_clock_era * kMediaClockModulus + UINT32_MAX - 9; - constexpr uint64_t target_ticks = (media_clock_era + 1) * kMediaClockModulus + 100; - constexpr uint64_t current_tai = MediaClockToNanoseconds(current_ticks); - constexpr uint64_t aligned_target_tai = - AlignToFrame(MediaClockToNanoseconds(target_ticks)); - ut_txa_set_user_pacing(ctx_, true); - ut_txa_set_cur_epochs(ctx_, current_tai / kFramePeriodNs); - ut_txa_set_mock_ptp_time(ctx_, current_tai); - ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); - uint64_t packet_tsc = 0; - - ASSERT_EQ( - ut_txa_run_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, 100, &packet_tsc), 0); - - EXPECT_EQ(packet_tsc, kCurrentTsc + aligned_target_tai - current_tai); - EXPECT_EQ(ut_txa_ptp_time_cursor(ctx_), aligned_target_tai); - ExpectNoPacingStats(); -} - -TEST_F(St40TxPacingTest, MediaClockExactTaskletFallsBackForPastWrappedTarget) { - constexpr uint64_t media_clock_era = kWrapTestMediaClockEra; - constexpr uint64_t current_ticks = (media_clock_era + 1) * kMediaClockModulus + 10; - constexpr uint64_t current_tai = MediaClockToNanoseconds(current_ticks); - constexpr uint64_t fallback_tai = ((current_tai / kFramePeriodNs) + 1) * kFramePeriodNs; - ut_txa_set_user_pacing(ctx_, true); - ut_txa_set_exact_user_pacing(ctx_, true); - ut_txa_set_cur_epochs(ctx_, current_tai / kFramePeriodNs); - ut_txa_set_mock_ptp_time(ctx_, current_tai); - ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); - uint64_t packet_tsc = 0; - - ASSERT_EQ(ut_txa_run_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, UINT32_MAX - 5, - &packet_tsc), - 0); - EXPECT_EQ(packet_tsc, kCurrentTsc + fallback_tai - current_tai); - EXPECT_EQ(ut_txa_ptp_time_cursor(ctx_), fallback_tai); + EXPECT_EQ(ut_txa_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, + media_clock_timestamp), + 0u); EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 1u); - EXPECT_EQ(ut_txa_notify_frame_done_epoch(ctx_), fallback_tai / kFramePeriodNs); - EXPECT_EQ(ut_txa_notify_frame_done_timestamp(ctx_), fallback_tai); - EXPECT_EQ(ut_txa_stat_epoch_mismatch(ctx_), 0u); - EXPECT_EQ(ut_txa_stat_epoch_drop(ctx_), 0u); - EXPECT_EQ(ut_txa_stat_epoch_onward(ctx_), 0u); -} - -TEST_F(St40TxPacingTest, MediaClockZeroUnwrapsAfterUint32WrapForUserPacing) { - constexpr uint64_t media_clock_era = kZeroTimestampMediaClockEra; - constexpr uint64_t current_ticks = media_clock_era * kMediaClockModulus + UINT32_MAX; - constexpr uint64_t target_ticks = (media_clock_era + 1) * kMediaClockModulus; - constexpr uint64_t current_tai = MediaClockToNanoseconds(current_ticks); - constexpr uint64_t target_tai = MediaClockToNanoseconds(target_ticks); - ut_txa_set_user_pacing(ctx_, true); - ut_txa_set_mock_ptp_time(ctx_, current_tai); - - EXPECT_EQ(ut_txa_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, 0), - target_tai); - ExpectNoPacingStats(); -} - -TEST_F(St40TxPacingTest, MediaClockZeroUnwrapsAfterUint32WrapForExactPacing) { - constexpr uint64_t media_clock_era = kZeroTimestampMediaClockEra; - constexpr uint64_t current_ticks = media_clock_era * kMediaClockModulus + UINT32_MAX; - constexpr uint64_t target_ticks = (media_clock_era + 1) * kMediaClockModulus; - constexpr uint64_t current_tai = MediaClockToNanoseconds(current_ticks); - constexpr uint64_t target_tai = MediaClockToNanoseconds(target_ticks); - ut_txa_set_user_pacing(ctx_, true); - ut_txa_set_exact_user_pacing(ctx_, true); - ut_txa_set_mock_ptp_time(ctx_, current_tai); - - EXPECT_EQ(ut_txa_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, 0), - target_tai); - ExpectNoPacingStats(); -} - -TEST_F(St40TxPacingTest, MediaClockZeroExactTaskletUsesWrappedFirstPacketTarget) { - constexpr uint64_t media_clock_era = kZeroTimestampMediaClockEra; - constexpr uint64_t current_ticks = media_clock_era * kMediaClockModulus + UINT32_MAX; - constexpr uint64_t target_ticks = (media_clock_era + 1) * kMediaClockModulus; - constexpr uint64_t current_tai = MediaClockToNanoseconds(current_ticks); - constexpr uint64_t target_tai = MediaClockToNanoseconds(target_ticks); - constexpr uint64_t target_epoch = (target_tai + kFramePeriodNs / 2) / kFramePeriodNs; - uint64_t packet_tsc = 0; - ut_txa_set_user_pacing(ctx_, true); - ut_txa_set_exact_user_pacing(ctx_, true); - ut_txa_set_mock_ptp_time(ctx_, current_tai); - ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc); - - ASSERT_EQ(ut_txa_prepare_frame_tasklet(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, 0, 1), 0); - EXPECT_EQ(ut_txa_step_frame_tasklet(ctx_), 0); - EXPECT_EQ(ut_txa_queued_packets(ctx_), 0u); - EXPECT_EQ(ut_txa_cur_epochs(ctx_), target_epoch); - ut_txa_set_mock_tsc_time(ctx_, kCurrentTsc + target_tai - current_tai); - EXPECT_EQ(ut_txa_step_frame_tasklet(ctx_), 1); - ASSERT_EQ(ut_txa_pop_packet_tsc(ctx_, &packet_tsc), 0); - EXPECT_EQ(packet_tsc, kCurrentTsc + target_tai - current_tai); - EXPECT_EQ(ut_txa_notify_frame_done_epoch(ctx_), target_epoch); - EXPECT_EQ(ut_txa_notify_frame_done_timestamp(ctx_), target_tai); - EXPECT_EQ(ut_txa_notify_frame_done_tfmt(ctx_), ST10_TIMESTAMP_FMT_TAI); - ExpectNoPacingStats(); } TEST_F(St40TxPacingTest, ExactTargetOneNanosecondBelowOneSecondWaits) { @@ -659,25 +517,6 @@ TEST_F(St40TxPacingTest, BackwardExactTargetFallsBackCompletionMetadata) { EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 1u); } -TEST_F(St40TxPacingTest, OversizedMediaClockUserPacingCountsOneError) { - ut_txa_set_user_pacing(ctx_, true); - - EXPECT_EQ(ut_txa_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, - (uint64_t)UINT32_MAX + 1), - 0u); - EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 1u); -} - -TEST_F(St40TxPacingTest, OversizedMediaClockExactPacingCountsOneError) { - ut_txa_set_user_pacing(ctx_, true); - ut_txa_set_exact_user_pacing(ctx_, true); - - EXPECT_EQ(ut_txa_pacing_required_tai(ctx_, ST10_TIMESTAMP_FMT_MEDIA_CLK, - (uint64_t)UINT32_MAX + 1), - 0u); - EXPECT_EQ(ut_txa_stat_error_user_timestamp(ctx_), 1u); -} - TEST_F(St40TxPacingTest, ExactTimestampNearUint64MaxSaturatesTscTarget) { ut_txa_set_user_pacing(ctx_, true); ut_txa_set_exact_user_pacing(ctx_, true); From 66d8ae728825bb1af55f573813f61846b25ab04a Mon Sep 17 00:00:00 2001 From: "Kasiewicz, Marek" Date: Tue, 14 Jul 2026 11:43:06 +0000 Subject: [PATCH 15/19] Docs: State TAI-only pacing on USER_PACING flags ST20_TX_FLAG_USER_PACING, ST22_TX_FLAG_USER_PACING, and ST40_TX_FLAG_USER_PACING (and their pipeline-level equivalents) only honor ST10_TIMESTAMP_FMT_TAI; ST10_TIMESTAMP_FMT_MEDIA_CLK falls back to default pacing. Document this on the flags and the tfmt fields of the associated frame meta structs. ST30_TX_FLAG_USER_PACING is left unchanged since audio genuinely supports media clock for pacing. Signed-off-by: Kasiewicz, Marek --- include/st20_api.h | 10 +++++++++- include/st40_api.h | 8 +++++++- include/st40_pipeline_api.h | 2 ++ include/st_pipeline_api.h | 4 ++++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/include/st20_api.h b/include/st20_api.h index c41ffcb88..770fdafa1 100644 --- a/include/st20_api.h +++ b/include/st20_api.h @@ -48,6 +48,8 @@ extern "C" { * User control the frame transmission time by passing a timestamp in * st20_tx_frame_meta.timestamp, lib will wait until timestamp is reached for each frame. * The time of sending is aligned with virtual receiver read schedule. + * Only ST10_TIMESTAMP_FMT_TAI is honored; ST10_TIMESTAMP_FMT_MEDIA_CLK is not + * supported for pacing and falls back to the default epoch-based pacing. */ #define ST20_TX_FLAG_USER_PACING (MTL_BIT32(3)) /** @@ -118,6 +120,8 @@ extern "C" { * Flag bit in flags of struct st22_tx_ops. * User control the frame pacing by pass a timestamp in st22_tx_frame_meta, * lib will wait until timestamp is reached for each frame. + * Only ST10_TIMESTAMP_FMT_TAI is honored; ST10_TIMESTAMP_FMT_MEDIA_CLK is not + * supported for pacing and falls back to the default epoch-based pacing. */ #define ST22_TX_FLAG_USER_PACING (MTL_BIT32(3)) /** @@ -407,7 +411,11 @@ struct st20_tx_frame_meta { enum st20_fmt fmt; /** Second field type indicate for interlaced mode, set by user */ bool second_field; - /** Timestamp format */ + /** + * Timestamp format. When ST20_TX_FLAG_USER_PACING is set, only + * ST10_TIMESTAMP_FMT_TAI is honored for pacing; ST10_TIMESTAMP_FMT_MEDIA_CLK + * falls back to the default epoch-based pacing. + */ enum st10_timestamp_fmt tfmt; /** Timestamp value */ uint64_t timestamp; diff --git a/include/st40_api.h b/include/st40_api.h index d6fe47328..fe183bbfd 100644 --- a/include/st40_api.h +++ b/include/st40_api.h @@ -41,6 +41,8 @@ typedef struct st_rx_ancillary_session_handle_impl* st40_rx_handle; * Flag bit in flags of struct st40_tx_ops. * User control the frame pacing by pass a timestamp in st40_tx_frame_meta, * lib will wait until timestamp is reached for each frame. + * Only ST10_TIMESTAMP_FMT_TAI is honored; ST10_TIMESTAMP_FMT_MEDIA_CLK is not + * supported for pacing and falls back to the default epoch-based pacing. */ #define ST40_TX_FLAG_USER_PACING (MTL_BIT32(3)) /** @@ -316,7 +318,11 @@ struct st40_frame { struct st40_tx_frame_meta { /** Frame fps */ enum st_fps fps; - /** Frame timestamp format */ + /** + * Frame timestamp format. When ST40_TX_FLAG_USER_PACING is set, only + * ST10_TIMESTAMP_FMT_TAI is honored for pacing; ST10_TIMESTAMP_FMT_MEDIA_CLK + * falls back to the default epoch-based pacing. + */ enum st10_timestamp_fmt tfmt; /** Frame timestamp value */ uint64_t timestamp; diff --git a/include/st40_pipeline_api.h b/include/st40_pipeline_api.h index ead0c2956..af43bd689 100644 --- a/include/st40_pipeline_api.h +++ b/include/st40_pipeline_api.h @@ -88,6 +88,8 @@ enum st40p_tx_flag { * Flag bit in flags of struct st40_tx_ops. * User control the frame pacing by pass a timestamp in st40_tx_frame_meta, * lib will wait until timestamp is reached for each frame. + * Only ST10_TIMESTAMP_FMT_TAI is honored; ST10_TIMESTAMP_FMT_MEDIA_CLK is not + * supported for pacing and falls back to the default epoch-based pacing. */ ST40P_TX_FLAG_USER_PACING = (MTL_BIT32(3)), /** diff --git a/include/st_pipeline_api.h b/include/st_pipeline_api.h index bf584034e..cdd01f715 100644 --- a/include/st_pipeline_api.h +++ b/include/st_pipeline_api.h @@ -384,6 +384,8 @@ enum st22p_tx_flag { /** * User control the frame pacing by pass a timestamp in st_frame, * lib will wait until timestamp is reached for each frame. + * Only ST10_TIMESTAMP_FMT_TAI is honored; ST10_TIMESTAMP_FMT_MEDIA_CLK is not + * supported for pacing and falls back to the default epoch-based pacing. */ ST22P_TX_FLAG_USER_PACING = (MTL_BIT32(3)), /** @@ -443,6 +445,8 @@ enum st20p_tx_flag { * User control frame transmission time by pass a timestamp in st_frame.timestamp, * lib will wait until timestamp is reached for each frame. The time of sending is * aligned with virtual receiver read schedule. + * Only ST10_TIMESTAMP_FMT_TAI is honored; ST10_TIMESTAMP_FMT_MEDIA_CLK is not + * supported for pacing and falls back to the default epoch-based pacing. */ ST20P_TX_FLAG_USER_PACING = (MTL_BIT32(3)), /** From 5d7694ddb5be16b7def8fb59e3ee57d082dd4848 Mon Sep 17 00:00:00 2001 From: "Kasiewicz, Marek" Date: Tue, 14 Jul 2026 11:48:40 +0000 Subject: [PATCH 16/19] Fix: Reject media clock timestamps for ST30 user pacing tx_audio_pacing_required_tai() converted a media-clock timestamp to nanoseconds with a bare ticks/rate multiplication, treating the tick count as elapsed time since zero instead of an absolute TAI instant. Since real TAI is on the order of 10^18 ns and a 32-bit tick count at audio sampling rates converts to at most ~10^14 ns, the result was always deep in the past relative to the current PTP time, so pacing silently collapsed to "send ASAP" on every frame instead of honoring the requested time. Reject ST10_TIMESTAMP_FMT_MEDIA_CLK for ST30_TX_FLAG_USER_PACING and fall back to default epoch-based pacing, matching ST20 and ST40. Documents the TAI-only restriction on the flag and the tfmt field of st30_tx_frame_meta. Signed-off-by: Kasiewicz, Marek --- include/st30_api.h | 8 +++++++- lib/src/st2110/st_tx_audio_session.c | 15 +++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/include/st30_api.h b/include/st30_api.h index 3a7979fff..9a6789c2c 100644 --- a/include/st30_api.h +++ b/include/st30_api.h @@ -42,6 +42,8 @@ typedef struct st_rx_audio_session_handle_impl* st30_rx_handle; * Flag bit in flags of struct st30_tx_ops. * User control the frame pacing by pass a timestamp in st30_tx_frame_meta, * lib will wait until timestamp is reached for each frame. + * Only ST10_TIMESTAMP_FMT_TAI is honored; ST10_TIMESTAMP_FMT_MEDIA_CLK is not + * supported for pacing and falls back to the default epoch-based pacing. */ #define ST30_TX_FLAG_USER_PACING (MTL_BIT32(3)) /** @@ -259,7 +261,11 @@ struct st30_tx_frame_meta { enum st30_sampling sampling; /** Session packet time */ enum st30_ptime ptime; - /** Frame timestamp format */ + /** + * Frame timestamp format. When ST30_TX_FLAG_USER_PACING is set, only + * ST10_TIMESTAMP_FMT_TAI is honored for pacing; ST10_TIMESTAMP_FMT_MEDIA_CLK + * falls back to the default epoch-based pacing. + */ enum st10_timestamp_fmt tfmt; /** Frame timestamp value */ uint64_t timestamp; diff --git a/lib/src/st2110/st_tx_audio_session.c b/lib/src/st2110/st_tx_audio_session.c index 81536af3f..ae5d3bb0e 100644 --- a/lib/src/st2110/st_tx_audio_session.c +++ b/lib/src/st2110/st_tx_audio_session.c @@ -244,22 +244,17 @@ static inline uint32_t tx_audio_pacing_time_stamp( static uint64_t tx_audio_pacing_required_tai(struct st_tx_audio_session_impl* s, enum st10_timestamp_fmt tfmt, uint64_t timestamp) { - uint64_t required_tai = 0; - if (!(s->ops.flags & ST30_TX_FLAG_USER_PACING)) return 0; if (!timestamp) return 0; if (tfmt == ST10_TIMESTAMP_FMT_MEDIA_CLK) { - if (timestamp > 0xFFFFFFFF) { - err("%s(%d), invalid timestamp %" PRIu64 "\n", __func__, s->idx, timestamp); - } - required_tai = - st10_media_clk_to_ns((uint32_t)timestamp, st30_get_sample_rate(s->ops.sampling)); - } else { - required_tai = timestamp; + s->port_user_stats.common.stat_error_user_timestamp++; + err("%s(%d), Media clock can't be used for user-controlled pacing\n", __func__, + s->idx); + return 0; /* invalid timestamp, fallback to default pacing */ } - return required_tai; + return timestamp; } static int tx_audio_session_sync_pacing(struct mtl_main_impl* impl, From e6c8693c9f888ed510fe9204ac8f314ba4f46bf1 Mon Sep 17 00:00:00 2001 From: "Kasiewicz, Marek" Date: Tue, 14 Jul 2026 11:53:47 +0000 Subject: [PATCH 17/19] Fix: Resolve SEGV in St20/St30 PipelineRxTest transport stubs st20p_harness.c and st30p_harness.c each define their own st20_rx_put_framebuff()/st30_rx_put_framebuff() (and get/reset stats) stubs to avoid touching the real transport layer, but session/ st20_harness.c and session/st30_harness.c pull in the real definitions of the same symbols by #include-ing the production st_rx_video_session.c / st_rx_audio_session.c into the same UnitTest binary. -Wl,--allow-multiple-definition silently resolves the clash by link order instead of erroring, and it picked the real, HW-backed symbol over the harness stub. The pipeline put_frame() path then called the real st20_rx_put_framebuff()/st30_rx_put_framebuff() with the harness's fake ctx->transport = 0x1 handle. MT_HANDLE_GUARD dereferenced that fake handle's type field, reading address 0x9 and crashing with SIGSEGV under ASan. Redirect the production pipeline .c's calls to distinctly named local stub functions via a #define around the #include, so the two definitions can never collide as global symbols regardless of link order. Signed-off-by: Kasiewicz, Marek --- tests/unit/pipeline/st20p_harness.c | 32 ++++++++++++++++++++++------- tests/unit/pipeline/st30p_harness.c | 30 ++++++++++++++++++++------- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/tests/unit/pipeline/st20p_harness.c b/tests/unit/pipeline/st20p_harness.c index 2e03ea813..81e500467 100644 --- a/tests/unit/pipeline/st20p_harness.c +++ b/tests/unit/pipeline/st20p_harness.c @@ -18,34 +18,52 @@ #include #include -#define st20_rx_get_session_stats ut20p_rx_get_session_stats -#define st20_rx_put_framebuff ut20p_rx_put_framebuff -#define st20_rx_reset_session_stats ut20p_rx_reset_session_stats - /* * Include the production pipeline .c so static rx_st20p_frame_ready() * is reachable and the public st20p_rx_* entry points see our private * struct. Disable USDT to avoid linker references to probe semaphores. + * + * The pipeline calls transport-layer st20_rx_* entry points that are + * also defined for real by st_rx_video_session.c (pulled into this same + * binary via session/st20_harness.c). Redirect them to distinctly named + * local stubs at compile time so the two definitions can never collide + * as global symbols — relying on the linker's multiple-definition + * resolution order is fragile and previously picked the real, + * HW-backed symbol here, which dereferenced our fake transport handle + * and crashed. */ +#define st20_rx_put_framebuff ut20p_stub_put_framebuff +#define st20_rx_get_session_stats ut20p_stub_get_session_stats +#define st20_rx_reset_session_stats ut20p_stub_reset_session_stats #undef MTL_HAS_USDT #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-variable" #include "st2110/pipeline/st20_pipeline_rx.c" #pragma GCC diagnostic pop +#undef st20_rx_put_framebuff +#undef st20_rx_get_session_stats +#undef st20_rx_reset_session_stats #undef st20_rx_get_session_stats #undef st20_rx_put_framebuff #undef st20_rx_reset_session_stats #include "common/ut_common.h" +/* + * Stubs for libmtl symbols called by the pipeline that would otherwise + * resolve to the real library (which requires HW/hugepages or a real + * transport session). Named distinctly from the real st20_rx_* symbols + * and wired in via the #define redirection above. + */ -int ut20p_rx_put_framebuff(st20_rx_handle handle, void* frame) { +int ut20p_stub_put_framebuff(st20_rx_handle handle, void* frame) { (void)handle; (void)frame; /* transport-side framebuf release is a no-op for the harness */ return 0; } -int ut20p_rx_get_session_stats(st20_rx_handle handle, struct st20_rx_user_stats* stats) { +int ut20p_stub_get_session_stats(st20_rx_handle handle, + struct st20_rx_user_stats* stats) { (void)handle; /* Return zeroed transport stats; the pipeline overlays its own * frame-level counters on top, which is exactly what we want to test. */ @@ -53,7 +71,7 @@ int ut20p_rx_get_session_stats(st20_rx_handle handle, struct st20_rx_user_stats* return 0; } -int ut20p_rx_reset_session_stats(st20_rx_handle handle) { +int ut20p_stub_reset_session_stats(st20_rx_handle handle) { (void)handle; return 0; } diff --git a/tests/unit/pipeline/st30p_harness.c b/tests/unit/pipeline/st30p_harness.c index f8e1099ec..62f1d3d69 100644 --- a/tests/unit/pipeline/st30p_harness.c +++ b/tests/unit/pipeline/st30p_harness.c @@ -10,15 +10,27 @@ #include #include -#define st30_rx_get_session_stats ut30p_rx_get_session_stats -#define st30_rx_put_framebuff ut30p_rx_put_framebuff -#define st30_rx_reset_session_stats ut30p_rx_reset_session_stats - #undef MTL_HAS_USDT +/* + * The pipeline calls transport-layer st30_rx_* entry points that are + * also defined for real by st_rx_audio_session.c (pulled into this same + * binary via session/st30_harness.c). Redirect them to distinctly named + * local stubs at compile time so the two definitions can never collide + * as global symbols — relying on the linker's multiple-definition + * resolution order is fragile and previously picked the real, + * HW-backed symbol here, which dereferenced our fake transport handle + * and crashed. + */ +#define st30_rx_put_framebuff ut30p_stub_put_framebuff +#define st30_rx_get_session_stats ut30p_stub_get_session_stats +#define st30_rx_reset_session_stats ut30p_stub_reset_session_stats #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-variable" #include "st2110/pipeline/st30_pipeline_rx.c" #pragma GCC diagnostic pop +#undef st30_rx_put_framebuff +#undef st30_rx_get_session_stats +#undef st30_rx_reset_session_stats #undef st30_rx_get_session_stats #undef st30_rx_put_framebuff @@ -26,19 +38,23 @@ #include "common/ut_common.h" -int ut30p_rx_put_framebuff(st30_rx_handle handle, void* frame) { +/* libmtl stubs, named distinctly from the real st30_rx_* symbols and + * wired in via the #define redirection above. */ + +int ut30p_stub_put_framebuff(st30_rx_handle handle, void* frame) { (void)handle; (void)frame; return 0; } -int ut30p_rx_get_session_stats(st30_rx_handle handle, struct st30_rx_user_stats* stats) { +int ut30p_stub_get_session_stats(st30_rx_handle handle, + struct st30_rx_user_stats* stats) { (void)handle; if (stats) memset(stats, 0, sizeof(*stats)); return 0; } -int ut30p_rx_reset_session_stats(st30_rx_handle handle) { +int ut30p_stub_reset_session_stats(st30_rx_handle handle) { (void)handle; return 0; } From 280d4fa0ebe23b513ea243c5cce30eee259476b7 Mon Sep 17 00:00:00 2001 From: "Kasiewicz, Marek" Date: Tue, 14 Jul 2026 13:44:00 +0000 Subject: [PATCH 18/19] Fix: Enable ptp service for TSN pacing NoCtx epoch test dev_if_init_pacing() rejects ST21_TX_PACING_WAY_TSN unless the built-in ptp service is enabled (MTL_FLAG_PTP_ENABLE), since the launch-time timestamp is only meaningful in the phc-synced clock domain. The st20p_tx_epoch_onward_recovers_after_ptp_step_pf_tsn_pacing case never set this flag, so mtl_init() failed with -EINVAL before the repro could even run. Set the flag for the TSN pacing_way only; TSC pacing does not compare against the phc and needs no ptp service. Signed-off-by: Kasiewicz, Marek --- .../noctx/testcases/st20p_ptp_epoch_recovery_tests.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/integration_tests/noctx/testcases/st20p_ptp_epoch_recovery_tests.cpp b/tests/integration_tests/noctx/testcases/st20p_ptp_epoch_recovery_tests.cpp index 3b927905b..4e76f6c6d 100644 --- a/tests/integration_tests/noctx/testcases/st20p_ptp_epoch_recovery_tests.cpp +++ b/tests/integration_tests/noctx/testcases/st20p_ptp_epoch_recovery_tests.cpp @@ -54,6 +54,9 @@ void RunEpochOnwardRecoveryCase(NoCtxTest* self, struct st_tests_context* ctx, ctx->para.ptp_get_time_fn = EpochRecoveryPtpClockNow; ctx->para.log_level = MTL_LOG_LEVEL_INFO; ctx->para.flags &= ~MTL_FLAG_DEV_AUTO_START_STOP; + /* TSN launch-time pacing compares against the phc, which only the + * built-in ptp service (fed by ptp_get_time_fn above) disciplines. */ + if (pacing_way == ST21_TX_PACING_WAY_TSN) ctx->para.flags |= MTL_FLAG_PTP_ENABLE; /* Shorten the periodic "M T DEV STATE" stat dump (default 10s) so the run * below comfortably covers several dumps without needing a long sleep. */ ctx->para.dump_period_s = 2; From 73c604985cd52c901b913ac63b23bbf128a89309 Mon Sep 17 00:00:00 2001 From: "Kasiewicz, Marek" Date: Wed, 15 Jul 2026 10:33:09 +0000 Subject: [PATCH 19/19] Test: Add wire-level TSN pacing spread smoke test Adds st20p_tx_packets_are_spread_over_frame_pf_tsn_pacing, which uses E830 RX hardware timestamps (st20_rx_tp_meta::ipt_avg via ST20P_RX_FLAG_TIMING_PARSER_META) to confirm TSN launch-time pacing actually spreads packets across the frame interval on the wire, instead of only completing session setup like the existing epoch recovery test. CollectPacingSpanRatios waits up to 10s for the first complete RX frame before starting a 6s steady-state sampling window, since real PF hardware RX session establishment latency varies independently of steady-state frame arrival. Signed-off-by: Kasiewicz, Marek --- .../st20p_ptp_epoch_recovery_tests.cpp | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/tests/integration_tests/noctx/testcases/st20p_ptp_epoch_recovery_tests.cpp b/tests/integration_tests/noctx/testcases/st20p_ptp_epoch_recovery_tests.cpp index 4e76f6c6d..f281c4f52 100644 --- a/tests/integration_tests/noctx/testcases/st20p_ptp_epoch_recovery_tests.cpp +++ b/tests/integration_tests/noctx/testcases/st20p_ptp_epoch_recovery_tests.cpp @@ -2,9 +2,11 @@ * Copyright(c) 2025 Intel Corporation */ +#include #include #include #include +#include #include "core/constants.hpp" #include "core/test_fixture.hpp" @@ -104,6 +106,63 @@ void RunEpochOnwardRecoveryCase(NoCtxTest* self, struct st_tests_context* ctx, << ") -- TX pacing never recovered from an epoch initialized ahead of real time"; } +/* Waits for the first complete RX frame with a generous bound, since real PF + * hardware RX flow/ARP establishment latency (not sampling-window size) is + * what actually varies run to run -- confirmed via RX_VIDEO_SESSION logging + * 0 pkts for the whole run when a fixed pre-sleep was used instead. Once + * flowing, discards kWarmupFrames more complete frames, then samples up to + * kMaxSampleFrames within a separate, steady-state-only deadline. Returns + * the total complete-frame count observed; span_ratios gets one + * observed/expected packet-train-span ratio per sampled frame with adequate + * st20_rx_tp_meta coverage. */ +int CollectPacingSpanRatios(NoCtxTest::St20pHandlerBundle& bundle, double trs_ns, + std::vector& span_ratios) { + constexpr int kWarmupFrames = 5; + constexpr int kMaxSampleFrames = 20; + st20p_rx_handle rx = bundle.handler->sessionsHandleRx; + int complete_frames = 0; + int sampled_frames = 0; + + auto first_frame_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (complete_frames == 0 && + std::chrono::steady_clock::now() < first_frame_deadline) { + struct st_frame* frame = st20p_rx_get_frame(rx); + if (!frame) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + continue; + } + if (frame->status == ST_FRAME_STATUS_COMPLETE) complete_frames++; + st20p_rx_put_frame(rx, frame); + } + + auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(6); + while (sampled_frames < kMaxSampleFrames && + std::chrono::steady_clock::now() < deadline) { + struct st_frame* frame = st20p_rx_get_frame(rx); + if (!frame) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + continue; + } + if (frame->status != ST_FRAME_STATUS_COMPLETE) { + st20p_rx_put_frame(rx, frame); + continue; + } + + complete_frames++; + if (complete_frames > kWarmupFrames) { + sampled_frames++; + struct st20_rx_tp_meta* tp = frame->tp[MTL_SESSION_PORT_P]; + if (tp && tp->pkts_cnt >= 2 && tp->pkts_cnt >= frame->pkts_total / 2) { + double observed_span_ns = (double)tp->ipt_avg * (tp->pkts_cnt - 1); + double expected_span_ns = trs_ns * (tp->pkts_cnt - 1); + span_ratios.push_back(observed_span_ns / expected_span_ns); + } + } + st20p_rx_put_frame(rx, frame); + } + return complete_frames; +} + } /* namespace */ /* TSN pacing (launch-time offload) is only advertised by PF drivers; this @@ -124,3 +183,60 @@ TEST_F(NoCtxTest, st20p_tx_epoch_onward_recovers_after_ptp_step_pf_tsn_pacing) { TEST_F(NoCtxTest, st20p_tx_epoch_onward_recovers_after_ptp_step_tsc_pacing) { RunEpochOnwardRecoveryCase(this, ctx, ST21_TX_PACING_WAY_TSC); } + +/* Smoke-test: does TSN launch-time pacing actually spread packets across the + * frame interval on the wire, instead of merely completing session setup? + * Uses E830 PF RX hardware timestamps (st20_rx_tp_meta::ipt_avg, populated by + * ST20P_RX_FLAG_TIMING_PARSER_META with MTL_FLAG_ENABLE_HW_TIMESTAMP) so + * scheduler/poll jitter on the RX side cannot manufacture apparent spread. */ +TEST_F(NoCtxTest, st20p_tx_packets_are_spread_over_frame_pf_tsn_pacing) { + ctx->para.pacing = ST21_TX_PACING_WAY_TSN; + ctx->para.flags |= MTL_FLAG_PTP_ENABLE | MTL_FLAG_ENABLE_HW_TIMESTAMP; + ctx->para.flags &= ~(MTL_FLAG_PTP_SOURCE_TSC | MTL_FLAG_DEV_AUTO_START_STOP); + ctx->handle = mtl_init(&ctx->para); + ASSERT_TRUE(ctx->handle != nullptr); + + auto bundle = createSt20pHandlerBundle( + /*createTx=*/true, /*createRx=*/true, nullptr, [](St20pHandler* handler) { + handler->sessionsOpsRx.flags |= ST20P_RX_FLAG_TIMING_PARSER_META; + }); + ASSERT_NE(bundle.handler, nullptr); + + bundle.handler->startSessionTx(); + mtl_start(ctx->handle); + + double tr_offset_ns = 0, trs_ns = 0; + uint32_t vrx_pkts = 0; + ASSERT_EQ(st20p_tx_get_pacing_params(bundle.handler->sessionsHandleTx, &tr_offset_ns, + &trs_ns, &vrx_pkts), + 0); + ASSERT_GT(trs_ns, 0.0); + + std::vector span_ratios; + int complete_frames = CollectPacingSpanRatios(bundle, trs_ns, span_ratios); + + bundle.handler->stopSession(); + + constexpr int kMinUsableFrames = 15; + ASSERT_GE((int)span_ratios.size(), kMinUsableFrames) + << "only " << span_ratios.size() << " of " << complete_frames + << " complete RX frames had adequate timing-parser packet coverage"; + + int in_range = 0; + for (double ratio : span_ratios) { + if (ratio >= 0.5 && ratio <= 1.5) in_range++; + } + EXPECT_GE(in_range, (int)(span_ratios.size() * 0.8)) + << "fewer than 80% of sampled frames show a packet-train span within " + "[0.5, 1.5] of the TSN-paced expectation (trs_ns=" + << trs_ns << ")"; + + std::vector sorted_ratios(span_ratios); + std::sort(sorted_ratios.begin(), sorted_ratios.end()); + size_t mid = sorted_ratios.size() / 2; + double median = (sorted_ratios.size() % 2) + ? sorted_ratios[mid] + : (sorted_ratios[mid - 1] + sorted_ratios[mid]) / 2.0; + EXPECT_GE(median, 0.7); + EXPECT_LE(median, 1.3); +}