diff --git a/.github/agents/mtl-developer.agent.md b/.github/agents/mtl-developer.agent.md index e86143963..445f9e793 100644 --- a/.github/agents/mtl-developer.agent.md +++ b/.github/agents/mtl-developer.agent.md @@ -1,5 +1,5 @@ --- -description: "Writes production MTL C/C++ AND the gtest cases that pin its behavior, in one context window. Walks a six-gate test-first loop (knowledge → failing test → implement → green test → self-review → hand off). Use for: any code change to lib/, include/, app/, plugins/, ecosystem/, or tests/** — bug fixes, new features, regressions, behavior changes; building (./build.sh / ninja -C build / ./format-coding.sh); running unit gtest (./build/tests/unit/UnitTest). Do NOT use for: running KahawaiTest against real VFs (→ MTL System Admin); host setup (→ MTL System Admin); pytest under tests/validation (→ main agent per validation-tests instruction); read-only Q&A (→ Explore); multi-subsystem orchestration (→ MTL Planner). Tools: editFiles, read, search, usages, problems, testFailure, todo, memory, execute, agent (Explore only). Requires: execute." +description: "Writes production MTL C/C++ AND the gtest cases that pin its behavior, in one context window. Walks a six-gate test-first loop (knowledge → failing test → implement → green test → self-review → hand off). Use for: any code change to lib/, include/, app/, plugins/, ecosystem/, or tests/** — bug fixes, new features, regressions, behavior changes; building (./build.sh / ninja -C build / ./format-coding.sh); running unit gtest (./build.sh unit). Do NOT use for: running KahawaiTest against real VFs (→ MTL System Admin); host setup (→ MTL System Admin); pytest under tests/validation (→ main agent per validation-tests instruction); read-only Q&A (→ Explore); multi-subsystem orchestration (→ MTL Planner). Tools: editFiles, read, search, usages, problems, testFailure, todo, memory, execute, agent (Explore only). Requires: execute." name: "MTL Developer (TDD)" tools: ['editFiles', 'read', 'codebase', 'search', 'usages', 'problems', 'testFailure', 'todo', 'memory', 'execute', 'agent'] agents: ['Explore'] @@ -39,7 +39,7 @@ C coding rules, KB routing, gtest conventions, and the test-tier picker auto-loa |---|---| | Edit `lib/`, `include/`, `app/`, `plugins/`, `ecosystem/`, `tests/unit/`, `tests/integration_tests/` | Configure host — hugepages, VFs, drivers (delegate to MTL System Admin) | | Build (`./build.sh`, `ninja -C build`, `./format-coding.sh`) | Run integration `KahawaiTest` against real VFs (delegate to MTL System Admin) | -| Run unit gtest (`./build/tests/unit/UnitTest`) | Run pytest under `tests/validation/` (return to main agent) | +| Run unit gtest (`./build.sh unit`) | Run pytest under `tests/validation/` (return to main agent) | ## The six-gate loop diff --git a/.github/agents/mtl-system-admin.agent.md b/.github/agents/mtl-system-admin.agent.md index fc390572b..ff0718171 100644 --- a/.github/agents/mtl-system-admin.agent.md +++ b/.github/agents/mtl-system-admin.agent.md @@ -1,5 +1,5 @@ --- -description: "Configures MTL hosts via MCP tools (no shell). Use for: post-reboot setup (hugepages, VFs, MtlManager), driver install/rebuild (DPDK, ICE), NIC bind/unbind, building MTL via `build_mtl`, running integration gtest `KahawaiTest` via `run_gtest` MCP tool against real VFs. This is the **enforced exit gate** for data-plane / session-lifecycle changes — MTL Developer (TDD) hands off here at Gate 6. Do NOT use for: running unit gtest `./build/tests/unit/UnitTest` (→ MTL Developer (TDD)); editing source code (→ MTL Developer (TDD)); arbitrary shell commands. Tools: `tool_search` (loads MCP tools), `todo`, MCP `mcp_mtl-system-se_*`. Requires: MCP server reachable." +description: "Configures MTL hosts via MCP tools (no shell). Use for: post-reboot setup (hugepages, VFs, MtlManager), driver install/rebuild (DPDK, ICE), NIC bind/unbind, building MTL via `build_mtl`, running integration gtest `KahawaiTest` via `run_gtest` MCP tool against real VFs. This is the **enforced exit gate** for data-plane / session-lifecycle changes — MTL Developer (TDD) hands off here at Gate 6. Do NOT use for: running unit gtest `./build.sh unit` (→ MTL Developer (TDD)); editing source code (→ MTL Developer (TDD)); arbitrary shell commands. Tools: `tool_search` (loads MCP tools), `todo`, MCP `mcp_mtl-system-se_*`. Requires: MCP server reachable." name: "MTL System Admin" tools: ['tool_search', 'todo', 'mtl-system-setup/*'] user-invocable: true diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index e196ade01..783133349 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -43,7 +43,7 @@ Use this table to pick the right subagent. Resolve ambiguity in order: | Multi-step work crossing 2+ subsystems (code + host + manager + plugins…) | **MTL Planner** | Decomposes and routes; no execution | | Edit any of `lib/`, `include/`, `app/`, `plugins/`, `ecosystem/`, `tests/unit/`, `tests/integration_tests/` | **MTL Developer (TDD)** | Owns code + tests + the six-gate TDD loop in one context window | | Build (`./build.sh`, `ninja -C build`, `./format-coding.sh`) | **MTL Developer (TDD)** | Build is Gate 4 of its loop | -| Run `./build/tests/unit/UnitTest` (unit gtest, no NIC) | **MTL Developer (TDD)** | Test runs are Gate 2 (fail) and Gate 4 (pass) | +| Run `./build.sh unit` (unit gtest, no NIC) | **MTL Developer (TDD)** | Test runs are Gate 2 (fail) and Gate 4 (pass) | | Run `KahawaiTest` (integration gtest, real VFs) | **MTL System Admin** | Only one with `run_gtest` MCP tool; enforced Gate 6 for data-plane changes | | Host setup (hugepages, VFs, drivers, MtlManager) | **MTL System Admin** | MCP-only, no shell | | Adversarial review of a saved diff | **MTL Reviewer** | Read-only; enforced Gate 5; refuses if diff empty | diff --git a/.github/instructions/mtl-system-setup.instructions.md b/.github/instructions/mtl-system-setup.instructions.md index 7dd965507..0c77efa8b 100644 --- a/.github/instructions/mtl-system-setup.instructions.md +++ b/.github/instructions/mtl-system-setup.instructions.md @@ -20,10 +20,11 @@ decision logic and domain knowledge that tool descriptions alone don't convey. 4. If MTL not built or rebuild needed → `build_mtl` or `mtl_clean_rebuild` ### "Run integration tests" -1. Verify prerequisites: DPDK installed, VFs bound, MtlManager running +1. Verify prerequisites **before** the first `run_gtest` call: `dpdk_devbind_status` (ports bound as VFs, not bare PFs — see below), MtlManager running (`manager_start` if not). 2. `run_gtest` with appropriate `gtest_filter` (e.g. `St20p*`, `St30*`) 3. If tests segfault → see "Test crashed" below -4. Report pass/fail counts +4. If RX session creation fails with multicast-join/IGMP errors → see "Multicast join / IGMP failure" below +5. Report pass/fail counts ### "Test crashed / segfault" 1. **SEGFAULT in `iavf_tm_node_add`** → `ice_driver_status` — almost always stock ICE @@ -31,6 +32,12 @@ decision logic and domain knowledge that tool descriptions alone don't convey. 3. `dpdk_status` → version mismatch? 4. `mtl_clean_rebuild` → stale build? +### "Multicast join / IGMP failure on RX session creation" +1. `dpdk_devbind_status` first — check if the port is a bare PF bound to `vfio-pci` with 0 VFs. E810/E830 must always run through VFs (see `nic_bind_pmd`'s own tool description: prefer `nic_create_vf` for E800 series); a bare PF-to-vfio-pci binding causes exactly this symptom. +2. Fix: `nic_bind_kernel` on the PF, then `nic_create_vf` (use `trusted=true` if the test needs multicast/promiscuous features). +3. Re-run `run_gtest` with the newly created VF BDFs as `p_port`/`r_port`. +4. This is an environment issue, not a code regression — confirm by reproducing on one unrelated, already-passing suite (e.g. `St30_rx.create_free_single`) only if you need extra certainty; don't use it as the primary diagnostic path, step 1 above is sufficient and faster. + ### "Build failed" 1. **Permission errors / CMakeCache.txt** → `mtl_clean_rebuild` 2. **librte_*.so not found** → `sudo ldconfig` (or `dpdk_build` if DPDK not installed) diff --git a/.github/mcp/mtl_mcp_server.py b/.github/mcp/mtl_mcp_server.py index cdd08ba9b..0eb4df21e 100644 --- a/.github/mcp/mtl_mcp_server.py +++ b/.github/mcp/mtl_mcp_server.py @@ -115,6 +115,23 @@ def _save_test_log(name: str, content: str) -> Path: return log_path +def _summarize_output(name: str, out: str, tail_lines: int = 40) -> str: + """Save large command output to a log file, returning a short summary. + + Full build/install output can be thousands of lines — dumping it into a + tool result burns context for little benefit. This saves it to disk and + returns the log path, total line count (like `wc -l`), and just the + tail, so the caller can grep/read the file directly if more is needed. + """ + log_path = _save_test_log(name, out) + total_lines = len(out.splitlines()) + tail = "\n".join(out.splitlines()[-tail_lines:]) + return ( + f"- Full log: `{log_path}` ({total_lines} lines)\n" + f"### Output (last {tail_lines} lines)\n```\n{tail}\n```" + ) + + def _load_versions() -> dict[str, str]: """Parse versions.env into a dict.""" result: dict[str, str] = {} @@ -788,7 +805,8 @@ def ice_driver_rebuild() -> str: ) return ( - f"## ICE Driver Build\n{out}\n\n## Reload\n{reload_out}\n\n" + f"## ICE Driver Build\n{_summarize_output('ice_driver_rebuild', out)}\n\n" + f"## Reload\n{reload_out}\n\n" "## ⚠ Important: VFs Destroyed\n" "The ICE driver reload destroyed all existing VFs.\n" "You MUST re-create VFs before running any tests:\n" @@ -846,7 +864,10 @@ def dpdk_build() -> str: ver_after = _run_output( "pkg-config --modversion libdpdk 2>/dev/null || echo 'not installed'" ) - return f"## DPDK Build\n{out}\n\n## Result\nInstalled version: {ver_after}" + return ( + f"## DPDK Build\n{_summarize_output('dpdk_build', out)}\n\n" + f"## Result\nInstalled version: {ver_after}" + ) # =================================================================== @@ -899,7 +920,7 @@ def build_mtl(mode: str = "debugonly") -> str: libmtl = _run_output("ldconfig -p 2>/dev/null | grep libmtl || echo 'not found'") return ( - f"## MTL Build ({mode})\n{out}\n\n" + f"## MTL Build ({mode})\n{_summarize_output('build_mtl', out)}\n\n" f"## Artifacts\n- RxTxApp: {rxtxapp}\n- MtlManager: {manager}\n- libmtl: {libmtl}" ) @@ -1248,7 +1269,7 @@ def build_ffmpeg_plugin() -> str: "bash .github/scripts/setup_environment.sh", timeout=600, ) - return f"## FFmpeg Plugin Build\n{out}" + return f"## FFmpeg Plugin Build\n{_summarize_output('ffmpeg_plugin_build', out)}" @mcp.tool() @@ -1265,7 +1286,9 @@ def build_gstreamer_plugin() -> str: "bash .github/scripts/setup_environment.sh", timeout=600, ) - return f"## GStreamer Plugin Build\n{out}" + return ( + f"## GStreamer Plugin Build\n{_summarize_output('gstreamer_plugin_build', out)}" + ) # =================================================================== @@ -1291,7 +1314,7 @@ def install_dependencies() -> str: "bash .github/scripts/setup_environment.sh", timeout=300, ) - return f"## Install Dependencies\n{out}" + return f"## Install Dependencies\n{_summarize_output('install_dependencies', out)}" # =================================================================== @@ -1316,7 +1339,7 @@ def build_ebpf_xdp() -> str: "bash .github/scripts/setup_environment.sh", timeout=300, ) - return f"## eBPF/XDP Build\n{out}" + return f"## eBPF/XDP Build\n{_summarize_output('ebpf_xdp_build', out)}" # =================================================================== @@ -1495,14 +1518,8 @@ def run_gtest( if failures: summary += "\n### Failed Tests\n" + "\n".join(f"- {f}" for f in failures) - # Save full log - log_path = _save_test_log("gtest", out) - summary += f"\n- Full log: `{log_path}`" summary += sim_hint - - # Include last 40 lines for context - tail = "\n".join(out.splitlines()[-40:]) - return f"{summary}\n\n### Output (last 40 lines)\n```\n{tail}\n```" + return f"{summary}\n\n{_summarize_output('gtest', out)}" @mcp.tool() diff --git a/.github/skills/mtl-build/SKILL.md b/.github/skills/mtl-build/SKILL.md index 77e06e5fe..4d13d642d 100644 --- a/.github/skills/mtl-build/SKILL.md +++ b/.github/skills/mtl-build/SKILL.md @@ -14,6 +14,7 @@ Run from the repository root: | `./build.sh` | Release | Default. Optimized build for testing and deployment | | `./build.sh debug` | Debug + ASAN | Debugging with AddressSanitizer (`-O0 -g`). Slower but catches memory bugs | | `./build.sh debugonly` | Debug only | Debug symbols without ASAN. Faster than `debug` for general development | +| `./build.sh unit` | Unit gtest | Configures `build_unit/` with `-Denable_unit_tests=true`, builds it, and runs `tests/unit/UnitTest`. Combine with `debug` (e.g. `./build.sh debug unit`) to run under ASan via `LD_PRELOAD` | ## Format Code diff --git a/.github/skills/mtl-write-test/SKILL.md b/.github/skills/mtl-write-test/SKILL.md index 009d4dc00..4e9deac26 100644 --- a/.github/skills/mtl-write-test/SKILL.md +++ b/.github/skills/mtl-write-test/SKILL.md @@ -11,7 +11,7 @@ The hard part of testing MTL is **picking the right tier** and **asserting the r | Tier | Binary | Hardware | Can observe | |---|---|---|---| -| **Unit gtest** — `tests/unit/` | `UnitTest` (built only with `-Denable_unit_tests=true`; **not** built by `./build.sh`) | None — runs as regular user under ASan | RX-side session logic fed synthetic mbufs at the RFC 4175 / RTP payload layer: frame assembly, bitmap dedup, redundancy merge, stat counters, pipeline accounting | +| **Unit gtest** — `tests/unit/` | `UnitTest` (built with `-Denable_unit_tests=true`; via `./build.sh unit`) | None — runs as regular user under ASan | RX-side session logic fed synthetic mbufs at the RFC 4175 / RTP payload layer: frame assembly, bitmap dedup, redundancy merge, stat counters, pipeline accounting | | **Integration gtest** — `tests/integration_tests/` | `KahawaiTest` | Real VFs (≥2; ≥4 for redundant cases) | Full session lifecycle, TX+RX through the NIC, every pacing mode, multi-session, callbacks at real timing, SHA digests, DMA, RSS, kernel-socket, AF_XDP, virtio-user | | **NoCtx gtest** — `tests/integration_tests/noctx/` | `KahawaiTest --no_ctx` | Real VFs (per-case; redundant cases require ≥4) | Same as integration plus behaviors that depend on `mtl_init()` flags/callbacks the shared test context cannot supply. **One case per process** (DPDK EAL cannot re-init) — runner script enforces this | | **pytest single-host** — `tests/validation/tests/single/` | RxTxApp / FFmpeg / GStreamer driven through pytest | Full host: VFs, MtlManager, NFS media, plugins, venv, SSH-to-localhost | End-to-end app behavior on one host: CLI flags, config files, real media bytes, PTP convergence | @@ -67,7 +67,7 @@ Trap: a library bug whose easiest reproducer is RxTxApp still belongs in a **gte 1. **Pick a neighbour test in the same directory** — closest to your behavior first, else the most recently modified file. Avoid `DISABLED_` cases and any file noticeably older than its siblings. **Copy its conventions, including its marker set** — markers gate CI selection ([tests/validation/pytest.ini](tests/validation/pytest.ini)) and an unmarked or wrongly-marked pytest runs in no job. 2. **Modify assertions only** until the test encodes your one requirement. 3. **Register in the build** for gtest tiers (add to the relevant `*_sources` list in the nearest `meson.build`). pytest needs no registration. -4. **Build:** `./build.sh` for integration binaries; for unit, `meson setup build_unit -Denable_unit_tests=true` once, then `ninja -C build_unit`. See [`/mtl-build`](.github/skills/mtl-build/SKILL.md). +4. **Build:** `./build.sh` for integration binaries; `./build.sh unit` for the unit suite (builds `build_unit/` and runs `UnitTest`). See [`/mtl-build`](.github/skills/mtl-build/SKILL.md). 5. **Run** with the binary / pytest invocation from the tier's auto-loaded instructions. 6. **Confirm** the test fails before the fix (regression) or passes deterministically across 3+ consecutive runs (new coverage). diff --git a/build.sh b/build.sh index 2dff4754a..401b0e2b2 100755 --- a/build.sh +++ b/build.sh @@ -8,7 +8,7 @@ set -e user=$(whoami) function usage() { - echo "Usage: $0 [debug|debugonly|debugoptimized|plain|release] [enable_fuzzing]" + echo "Usage: $0 [debug|debugonly|debugoptimized|plain|release] [enable_fuzzing] [unit]" exit 0 } @@ -17,6 +17,7 @@ enable_asan=false enable_tap=false enable_usdt=true enable_fuzzing=false +build_unit=false : "${MTL_BUILD_ENABLE_ASAN:=false}" : "${MTL_BUILD_ENABLE_TAP:=false}" @@ -67,6 +68,9 @@ while [ $# -gt 0 ]; do enable_fuzzing=true echo "Enable fuzzers" ;; + "unit") + build_unit=true + ;; *) usage ;; @@ -82,6 +86,22 @@ PLUGINS_BUILD_DIR=${WORKSPACE}/build/plugins LD_PRELOAD_BUILD_DIR=${WORKSPACE}/build/ld_preload MANAGER_BUILD_DIR=${WORKSPACE}/build/manager RXTXAPP_BUILD_DIR=${WORKSPACE}/tests/tools/RxTxApp/build +UNIT_BUILD_DIR=${WORKSPACE}/build_unit + +# build and run the unit gtest suite (tests/unit), then exit +if [ "$build_unit" == "true" ]; then + meson setup "${UNIT_BUILD_DIR}" -Dbuildtype="$buildtype" -Denable_asan="$enable_asan" -Denable_unit_tests=true + ninja -C "${UNIT_BUILD_DIR}" + + if [ "$enable_asan" == "true" ]; then + ASAN_LIBRARY=$(cc -print-file-name=libasan.so) + LD_PRELOAD="${ASAN_LIBRARY}${LD_PRELOAD:+:${LD_PRELOAD}}" \ + "${UNIT_BUILD_DIR}/tests/unit/UnitTest" + else + "${UNIT_BUILD_DIR}/tests/unit/UnitTest" + fi + exit 0 +fi if [ -n "${MTL_INSTALL_PREFIX:-}" ]; then MTL_PREFIX_ARGS="--prefix=$MTL_INSTALL_PREFIX" diff --git a/doc/design.md b/doc/design.md index 6c792b7fa..8055ce2eb 100644 --- a/doc/design.md +++ b/doc/design.md @@ -354,7 +354,18 @@ st40p_rx_put_frame ``` - **TX pipeline (`st40p_tx_*`)** wraps the lower-level `st40_tx_*` APIs while honoring redundancy, user timestamps, and user-managed frame buffers (for example when `ST40P_TX_FLAG_EXT_FRAME` is enabled). The helper also exposes the maximum user data word (UDW) size via `st40p_tx_max_udw_buff_size()` so that producers can pack metadata deterministically. -- **RX pipeline (`st40p_rx_*`)** assembles ancillary packets into user buffers and can optionally dump metadata/UDW buffers directly to files for debugging through `st40p_rx_set_dump`. Each RX callback receives both the metadata blocks and the captured payload, mirroring the TX layout to simplify round-trip validation. +- **RX pipeline (`st40p_rx_*`)** is a thin shim over the **FRAME_LEVEL transport** + (`ST40_TYPE_FRAME_LEVEL` on `st40_rx_ops`). The transport owns the frame-buffer pool, + parses RFC 8331 packets, and assembles full frames using a 64-bit per-frame + **session-merged sequence bitmap** that natively absorbs cross-port redundancy. + Once a frame is complete the transport invokes + `notify_frame_ready(priv, addr, struct st40_rx_frame_meta*)`; the pipeline records + the buffer pointer plus all stats (timestamps, per-port and session-level + `seq_lost`/`seq_discont`, `pkts_total`, `pkts_recv[]`, `status`) into its slot — + zero-copy. The app returns ownership through `st40p_rx_put_frame`, which calls + `st40_rx_put_framebuff(transport, addr)` under the hood. Apps that still want raw + RTP can fall back to `ST40_TYPE_RTP_LEVEL` and pull mbufs via + `st40_rx_get_mbuf`/`st40_rx_put_mbuf`. Split-mode ancillary (packet-split) is supported via `tx_split_anc_by_pkt` on TX and `rx_rtp_ring_size` plus `frame_info_path` on RX. Enable `tx_split_anc_by_pkt=true` diff --git a/doc/stats_guide.md b/doc/stats_guide.md index 4d48e6b00..7a418e193 100644 --- a/doc/stats_guide.md +++ b/doc/stats_guide.md @@ -337,6 +337,18 @@ ST40 anc) a fully-lost frame is absorbed — the delivered frame count drops rather than emitting an extra incomplete frame — so under heavy contiguous loss `stat_frames_corrupted` is a lower bound on frame-integrity impact. +> **ST40p RX seq stats are bitmap-derived (transport-side).** The +> `seq_lost` / `seq_discont` fields on `st40_frame_info` (and the underlying +> `st40_rx_frame_meta`) are computed in the RX session from a per-frame +> 64-bit **session-merged** sequence bitmap. Each accepted packet (after +> the per-port redundancy filter) flips its bit; at marker time +> `seq_lost = (max_offset + 1) - popcount(bitmap)`. Packets that arrive on +> the redundant port and fill a gap left by the primary port flip the same +> bit, so cross-port redundancy is reflected directly in the count — there +> is no false positive `seq_discont` from cross-port reorder. The +> `port_seq_lost[]` / `port_seq_discont[]` arrays still report the per-port +> intra-frame view for observability. + **TX (any type).** `stat_epoch_drop` = app handed frame too late, slots were skipped (counter advanced by the number of skipped slots). `stat_epoch_onward` = system clock ran past `max_onward_epochs` ahead of the diff --git a/ecosystem/gstreamer_plugin/README.md b/ecosystem/gstreamer_plugin/README.md index 597918891..9fce2660d 100644 --- a/ecosystem/gstreamer_plugin/README.md +++ b/ecosystem/gstreamer_plugin/README.md @@ -16,7 +16,7 @@ To install the required GStreamer packages on Ubuntu or Debian, run the followin ```bash sudo apt update -sudo apt install -y gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-tools gstreamer1.0-libav libgstreamer1.0-dev +sudo apt install -y gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-tools gstreamer1.0-libav libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev ``` > For MTL installation instructions please refer to [Build Documentation](../../doc/build.md). diff --git a/ecosystem/gstreamer_plugin/gst_mtl_common.c b/ecosystem/gstreamer_plugin/gst_mtl_common.c index 53d739a0e..37582e8c6 100644 --- a/ecosystem/gstreamer_plugin/gst_mtl_common.c +++ b/ecosystem/gstreamer_plugin/gst_mtl_common.c @@ -632,7 +632,7 @@ gboolean gst_mtl_common_parse_general_arguments(struct mtl_init_params* mtl_init GST_INFO("allow-down-ports enabled: MTL_FLAG_ALLOW_DOWN_PORTS set for all ports"); } - if (general_args->dma_dev && strlen(general_args->dma_dev)) { + if (strlen(general_args->dma_dev)) { gchar** dma_tokens = g_strsplit(general_args->dma_dev, ",", MTL_DMA_DEV_MAX + 1); gint idx = 0; diff --git a/ecosystem/gstreamer_plugin/gst_mtl_st40_rx.c b/ecosystem/gstreamer_plugin/gst_mtl_st40_rx.c deleted file mode 100644 index 81dd842e1..000000000 --- a/ecosystem/gstreamer_plugin/gst_mtl_st40_rx.c +++ /dev/null @@ -1,594 +0,0 @@ -/* - * GStreamer - * Copyright (C) 2005 Thomas Vander Stichele - * Copyright (C) 2005 Ronald S. Bultje - * Copyright (C) 2024 Intel Corporation - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - * - * Alternatively, the contents of this file may be used under the - * GNU Lesser General Public License Version 2.1 (the "LGPL"), in - * which case the following provisions apply instead of the ones - * mentioned above: - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 59 Temple Place - Suite 330, - * Boston, MA 02111-1307, USA. - */ - -/** - * SECTION:element-mtl_rx_src - * - * The mtl_rx_src element is a GStreamer src plugin designed to interface with - * the Media Transport Library (MTL). - * MTL is a software-based solution optimized for high-throughput, low-latency - * transmission and reception of media data. - * - * It includes a built-in SMPTE ST 2110-compliant - * implementation for Professional Media over Managed IP Networks. - * - * This element allows GStreamer pipelines to receive media data using the MTL, - * ensuring efficient and reliable media transport over IP networks. - * - */ - -#ifdef HAVE_CONFIG_H -#include -#endif - -#include "gst_mtl_st40_rx.h" - -GST_DEBUG_CATEGORY_STATIC(gst_mtl_st40_rx_debug); -#define GST_CAT_DEFAULT gst_mtl_st40_rx_debug - -#ifndef GST_LICENSE -#define GST_LICENSE "LGPL" -#endif - -#ifndef GST_API_VERSION -#define GST_API_VERSION "1.0" -#endif - -#ifndef GST_PACKAGE_NAME -#define GST_PACKAGE_NAME "Media Transport Library st2110 st40 rx plugin" -#endif - -#ifndef GST_PACKAGE_ORIGIN -#define GST_PACKAGE_ORIGIN "https://github.com/OpenVisualCloud/Media-Transport-Library" -#endif - -#ifndef PACKAGE -#define PACKAGE "gst-mtl-st40-rx" -#endif - -#ifndef PACKAGE_VERSION -#define PACKAGE_VERSION "1.0" -#endif - -enum { - PROP_ST40_RX_BUFFER_SIZE = PROP_GENERAL_MAX, - PROP_ST40_RX_TIMEOUT_MBUF_GET, - PROP_ST40_RX_INCLUDE_METADATA_IN_BUFFER, - PROP_MAX -}; - -/* pad template */ -static GstStaticPadTemplate gst_mtl_st40_rx_src_pad_template = - GST_STATIC_PAD_TEMPLATE("src", GST_PAD_SRC, GST_PAD_ALWAYS, GST_STATIC_CAPS_ANY); - -#define gst_mtl_st40_rx_parent_class parent_class -G_DEFINE_TYPE_WITH_CODE(Gst_Mtl_St40_Rx, gst_mtl_st40_rx, GST_TYPE_BASE_SRC, - GST_DEBUG_CATEGORY_INIT(gst_mtl_st40_rx_debug, "mtl_st40_rx", 0, - "MTL St2110 st40 transmission src")); - -GST_ELEMENT_REGISTER_DEFINE(mtl_st40_rx, "mtl_st40_rx", GST_RANK_NONE, - GST_TYPE_MTL_ST40_RX); - -static void gst_mtl_st40_rx_set_property(GObject* object, guint prop_id, - const GValue* value, GParamSpec* pspec); -static void gst_mtl_st40_rx_get_property(GObject* object, guint prop_id, GValue* value, - GParamSpec* pspec); -static void gst_mtl_st40_rx_finalize(GObject* object); - -static gboolean gst_mtl_st40_rx_start(GstBaseSrc* basesrc); -static GstFlowReturn gst_mtl_st40_rx_create(GstBaseSrc* basesrc, guint64 offset, - guint length, GstBuffer** buffer); - -static gint gst_mtl_st40_rx_mbuff_available(void* priv); -static void* gst_mtl_st40_rx_get_mbuf_with_timeout(Gst_Mtl_St40_Rx* src, - st40_rx_handle handle, void** usrptr, - uint16_t* size); -static GstFlowReturn gst_mtl_st40_rx_fill_buffer(Gst_Mtl_St40_Rx* src, GstBuffer** buf, - void* usrptr); -static guint gst_mtl_st40_rx_parse_port_arguments(struct st40_rx_ops* ops_rx, - SessionPortArgs* portArgs); -static struct st40_rfc8331_payload_hdr* gst_mtl_st40_rx_shift_payload_hdr( - struct st40_rfc8331_payload_hdr* payload_hdr, int udw_size); - -static gint gst_mtl_st40_rx_mbuff_available(void* priv) { - Gst_Mtl_St40_Rx* src = (Gst_Mtl_St40_Rx*)priv; - - pthread_mutex_lock(&(src->mbuff_mutex)); - pthread_cond_signal(&(src->mbuff_cond)); - pthread_mutex_unlock(&(src->mbuff_mutex)); - - return 0; -} - -static void gst_mtl_st40_rx_class_init(Gst_Mtl_St40_RxClass* klass) { - GObjectClass* gobject_class; - GstElementClass* gstelement_class; - GstBaseSrcClass* gstbasesrc_class; - - gobject_class = G_OBJECT_CLASS(klass); - gstelement_class = GST_ELEMENT_CLASS(klass); - gstbasesrc_class = GST_BASE_SRC_CLASS(klass); - - gst_element_class_set_metadata( - gstelement_class, "MtlRxSt40Src", "Src/Metadata", - "MTL transmission plugin for SMPTE ST 2110-40 standard (ancillary data))", - "Dawid Wesierski "); - - gst_element_class_add_static_pad_template(gstelement_class, - &gst_mtl_st40_rx_src_pad_template); - - gobject_class->set_property = GST_DEBUG_FUNCPTR(gst_mtl_st40_rx_set_property); - gobject_class->get_property = GST_DEBUG_FUNCPTR(gst_mtl_st40_rx_get_property); - gobject_class->finalize = GST_DEBUG_FUNCPTR(gst_mtl_st40_rx_finalize); - - gstbasesrc_class->start = GST_DEBUG_FUNCPTR(gst_mtl_st40_rx_start); - gstbasesrc_class->create = GST_DEBUG_FUNCPTR(gst_mtl_st40_rx_create); - - gst_mtl_common_init_general_arguments(gobject_class); - - g_object_class_install_property( - gobject_class, PROP_ST40_RX_BUFFER_SIZE, - g_param_spec_uint("buffer-size", "Buffer Size", - "Size of the buffer used for receiving data", 0, G_MAXUINT, 1024, - G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); - - g_object_class_install_property( - gobject_class, PROP_ST40_RX_TIMEOUT_MBUF_GET, - g_param_spec_uint("timeout", "Timeout for Mbuf", - "Timeout in seconds for getting mbuf", 0, G_MAXUINT, 10, - G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); - - g_object_class_install_property( - gobject_class, PROP_ST40_RX_INCLUDE_METADATA_IN_BUFFER, - g_param_spec_boolean("include-metadata-in-buffer", "Include Metadata in Buffer", - "Whether to include metadata in the output buffer", FALSE, - G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); -} - -static guint gst_mtl_st40_rx_parse_port_arguments(struct st40_rx_ops* ops_rx, - SessionPortArgs* port_args) { - guint mtl_port_idx = MTL_PORT_P; - - while (mtl_port_idx <= MTL_PORT_R && strlen(port_args->port[mtl_port_idx])) { - if (inet_pton(AF_INET, port_args->session_ip_string[mtl_port_idx], - ops_rx->ip_addr[mtl_port_idx]) != 1) { - GST_ERROR("Invalid destination IP address: %s", - port_args->session_ip_string[mtl_port_idx]); - return 0; - } - - strncpy(ops_rx->port[mtl_port_idx], port_args->port[mtl_port_idx], MTL_PORT_MAX_LEN); - - if ((port_args->udp_port[mtl_port_idx] < 0) || - (port_args->udp_port[mtl_port_idx] > 0xFFFF)) { - GST_ERROR("%s, invalid UDP port: %d\n", __func__, - port_args->udp_port[mtl_port_idx]); - return 0; - } - - ops_rx->udp_port[mtl_port_idx] = port_args->udp_port[mtl_port_idx]; - mtl_port_idx++; - } - - /* check primary port */ - if (strlen(port_args->port[MTL_PORT_P]) == 0) { - GST_ERROR("Invalid port number %u", mtl_port_idx); - return 0; - } - - if ((port_args->payload_type < 0) || (port_args->payload_type > 0x7F)) { - GST_ERROR("%s, invalid payload_type: %d\n", __func__, port_args->payload_type); - return 0; - } - - ops_rx->payload_type = port_args->payload_type; - - return mtl_port_idx; -} - -static gboolean gst_mtl_st40_rx_start(GstBaseSrc* basesrc) { - struct st40_rx_ops ops_rx = {0}; - - Gst_Mtl_St40_Rx* src = GST_MTL_ST40_RX(basesrc); - - GST_DEBUG_OBJECT(src, "start"); - GST_DEBUG("Media Transport Initialization start"); - - src->mtl_lib_handle = gst_mtl_common_init_handle(&(src->generalArgs), FALSE); - - if (!src->mtl_lib_handle) { - GST_ERROR("Could not initialize MTL"); - return FALSE; - } - - if (src->timeout_mbuf_get_seconds <= 0) { - src->timeout_mbuf_get_seconds = 10; - } else if (src->timeout_mbuf_get_seconds <= 3) { - GST_WARNING("Timeout for getting mbuf is too low, setting to 3 seconds"); - src->timeout_mbuf_get_seconds = 3; - } - ops_rx.name = "st40src"; - ops_rx.priv = basesrc; - ops_rx.notify_rtp_ready = gst_mtl_st40_rx_mbuff_available; - - if (src->mbuff_size) { - if (!IS_POWER_OF_2(src->mbuff_size)) { - GST_WARNING("Buffer size is not power of 2, setting to 1024"); - ops_rx.rtp_ring_size = 1024; - src->mbuff_size = 1024; - } else { - ops_rx.rtp_ring_size = src->mbuff_size; - } - } else { - ops_rx.rtp_ring_size = 1024; - src->mbuff_size = 1024; - } - - if (src->portArgs.payload_type == 0) { - ops_rx.payload_type = PAYLOAD_TYPE_ANCILLARY; - } else if ((src->portArgs.payload_type < 0) || (src->portArgs.payload_type > 0x7F)) { - GST_ERROR("%s, invalid payload_type: %d\n", __func__, src->portArgs.payload_type); - } else { - ops_rx.payload_type = src->portArgs.payload_type; - } - - if (pthread_mutex_init(&(src->mbuff_mutex), NULL) || - pthread_cond_init(&(src->mbuff_cond), NULL)) { - GST_ERROR("Failed to initialize mutex or condition variable"); - return FALSE; - } - - gst_mtl_common_copy_general_to_session_args(&(src->generalArgs), &(src->portArgs)); - - ops_rx.num_port = gst_mtl_st40_rx_parse_port_arguments(&ops_rx, &src->portArgs); - if (!ops_rx.num_port) { - GST_ERROR("Failed to parse port arguments"); - return FALSE; - } - - src->rx_handle = st40_rx_create(src->mtl_lib_handle, &ops_rx); - if (!src->rx_handle) { - GST_ERROR("Failed to create st40 rx handle"); - return FALSE; - } - - return TRUE; -} - -static void gst_mtl_st40_rx_init(Gst_Mtl_St40_Rx* src) { - GstElement* element = GST_ELEMENT(src); - GstPad* srcpad; - - srcpad = gst_element_get_static_pad(element, "src"); - if (!srcpad) { - GST_ERROR_OBJECT(src, "Failed to get src pad from child element"); - return; - } -} - -static void gst_mtl_st40_rx_set_property(GObject* object, guint prop_id, - const GValue* value, GParamSpec* pspec) { - Gst_Mtl_St40_Rx* self = GST_MTL_ST40_RX(object); - - if (prop_id < PROP_GENERAL_MAX) { - gst_mtl_common_set_general_arguments(object, prop_id, value, pspec, - &(self->generalArgs), &(self->portArgs)); - return; - } - - switch (prop_id) { - case PROP_ST40_RX_BUFFER_SIZE: - self->mbuff_size = g_value_get_uint(value); - break; - case PROP_ST40_RX_TIMEOUT_MBUF_GET: - self->timeout_mbuf_get_seconds = g_value_get_uint(value); - break; - case PROP_ST40_RX_INCLUDE_METADATA_IN_BUFFER: - self->include_metadata_in_buffer = g_value_get_boolean(value); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); - break; - } -} - -static void gst_mtl_st40_rx_get_property(GObject* object, guint prop_id, GValue* value, - GParamSpec* pspec) { - Gst_Mtl_St40_Rx* src = GST_MTL_ST40_RX(object); - - if (prop_id < PROP_GENERAL_MAX) { - gst_mtl_common_get_general_arguments(object, prop_id, value, pspec, - &(src->generalArgs), &(src->portArgs)); - return; - } - - switch (prop_id) { - case PROP_ST40_RX_BUFFER_SIZE: - g_value_set_uint(value, src->mbuff_size); - break; - case PROP_ST40_RX_TIMEOUT_MBUF_GET: - g_value_set_uint(value, src->timeout_mbuf_get_seconds); - break; - case PROP_ST40_RX_INCLUDE_METADATA_IN_BUFFER: - g_value_set_boolean(value, src->include_metadata_in_buffer); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); - break; - } -} - -static void* gst_mtl_st40_rx_get_mbuf_with_timeout(Gst_Mtl_St40_Rx* src, - st40_rx_handle handle, void** usrptr, - uint16_t* size) { - struct timespec ts; - gint ret; - void* mbuf; - - clock_gettime(CLOCK_REALTIME, &ts); - ts.tv_sec += src->timeout_mbuf_get_seconds; - pthread_mutex_lock(&(src->mbuff_mutex)); - ret = pthread_cond_timedwait(&(src->mbuff_cond), &(src->mbuff_mutex), &ts); - pthread_mutex_unlock(&(src->mbuff_mutex)); - - if (ret == ETIMEDOUT) { - GST_INFO("Timeout occurred while waiting for mbuf"); - return NULL; - } - - mbuf = st40_rx_get_mbuf(src->rx_handle, usrptr, size); - if (!mbuf) GST_ERROR("Failed to get ancillary mbuf\n"); - - return mbuf; -} - -static struct st40_rfc8331_payload_hdr* gst_mtl_st40_rx_shift_payload_hdr( - struct st40_rfc8331_payload_hdr* payload_hdr, int udw_size) { - gint package_size; - gint payload_len; - - package_size = ((WORD_10_BIT_ALIGN + udw_size) * USER_DATA_WORD_BIT_SIZE) / BYTE_SIZE; - payload_len = sizeof(struct st40_rfc8331_payload_hdr) - - (package_size % WORD_10_BIT_ALIGN) + package_size; - - return (struct st40_rfc8331_payload_hdr*)((uint8_t*)payload_hdr + payload_len); -} - -static GstFlowReturn gst_mtl_st40_rx_check_parity( - const struct st40_rfc8331_payload_hdr* payload_hdr) { - if (!st40_check_parity_bits(payload_hdr->second_hdr_chunk.did)) { - GST_ERROR("Parity check failed for DID"); - return GST_FLOW_ERROR; - } - if (!st40_check_parity_bits(payload_hdr->second_hdr_chunk.sdid)) { - GST_ERROR("Parity check failed for SDID"); - return GST_FLOW_ERROR; - } - if (!st40_check_parity_bits(payload_hdr->second_hdr_chunk.data_count)) { - GST_ERROR("Parity check failed for Data Count"); - return GST_FLOW_ERROR; - } - return GST_FLOW_OK; -} - -static GstFlowReturn gst_mtl_st40_rx_fill_buffer(Gst_Mtl_St40_Rx* src, GstBuffer** buffer, - void* usrptr) { - struct st40_rfc8331_rtp_hdr* hdr; - struct st40_rfc8331_payload_hdr* payload_hdr; - GstMapInfo dest_info; - guint16 data; - gint udw_size; - guint buffer_size = 0, meta_offset, anc_count; - - hdr = (struct st40_rfc8331_rtp_hdr*)usrptr; - anc_count = hdr->first_hdr_chunk.anc_count; - - if (anc_count > ST40_RFC8331_PAYLOAD_MAX_ANCILLARY_COUNT) { - GST_ERROR("Ancillary data count: %d must not be bigger then %d", anc_count, - ST40_RFC8331_PAYLOAD_MAX_ANCILLARY_COUNT); - return GST_FLOW_ERROR; - } - - if (anc_count == 0) { - *buffer = gst_buffer_new_allocate(NULL, 0, NULL); - if (!*buffer) { - GST_ERROR("Failed to allocate empty buffer"); - return GST_FLOW_ERROR; - } - return GST_FLOW_OK; - } - - /* local is fine anc_count will not be bigger than 20 and less than 0 */ - guint8* anc_data[anc_count]; - guint8 anc_data_count[anc_count]; - - payload_hdr = (struct st40_rfc8331_payload_hdr*)(&hdr[1]); - for (int i = 0; i < anc_count; i++) { - payload_hdr->swapped_first_hdr_chunk = ntohl(payload_hdr->swapped_first_hdr_chunk); - payload_hdr->swapped_second_hdr_chunk = ntohl(payload_hdr->swapped_second_hdr_chunk); - if (gst_mtl_st40_rx_check_parity(payload_hdr) != GST_FLOW_OK) { - for (int j = 0; j < i; j++) free(anc_data[j]); - return GST_FLOW_ERROR; - } - - udw_size = payload_hdr->second_hdr_chunk.data_count & 0xff; - anc_data_count[i] = udw_size; - meta_offset = 0; - buffer_size += udw_size; - - if (src->include_metadata_in_buffer) { - buffer_size += ST40_BYTE_SIZE_OF_PAYLOAD_METADATA; - anc_data[i] = malloc(udw_size + ST40_BYTE_SIZE_OF_PAYLOAD_METADATA); - } else { - anc_data[i] = malloc(udw_size); - } - - if (!anc_data[i]) { - GST_ERROR("Failed to allocate memory for ancillary data"); - for (int j = 0; j < i; j++) free(anc_data[j]); - return GST_FLOW_ERROR; - } - - if (src->include_metadata_in_buffer) { - anc_data[i][meta_offset++] = payload_hdr->first_hdr_chunk.c; - anc_data[i][meta_offset++] = payload_hdr->first_hdr_chunk.line_number; - anc_data[i][meta_offset++] = payload_hdr->first_hdr_chunk.horizontal_offset; - anc_data[i][meta_offset++] = payload_hdr->first_hdr_chunk.s; - anc_data[i][meta_offset++] = payload_hdr->first_hdr_chunk.stream_num; - anc_data[i][meta_offset++] = payload_hdr->second_hdr_chunk.did & 0xff; - anc_data[i][meta_offset++] = payload_hdr->second_hdr_chunk.sdid & 0xff; - anc_data[i][meta_offset++] = payload_hdr->second_hdr_chunk.data_count & 0xff; - } - - payload_hdr->swapped_second_hdr_chunk = htonl(payload_hdr->swapped_second_hdr_chunk); - - for (int d = 0; d < udw_size; d++) { - data = st40_get_udw(d + 3, (uint8_t*)&payload_hdr->second_hdr_chunk); - if (!st40_check_parity_bits(data)) { - GST_ERROR("Ancillary data parity bits check failed, data=0x%03x", data & 0x3FF); - for (int j = 0; j <= i; j++) free(anc_data[j]); - return GST_FLOW_ERROR; - } - anc_data[i][d + meta_offset] = data & 0xff; - } - - payload_hdr = gst_mtl_st40_rx_shift_payload_hdr(payload_hdr, udw_size); - } - - *buffer = gst_buffer_new_allocate(NULL, buffer_size, NULL); - if (!*buffer) { - GST_ERROR("Failed to allocate space for the buffer"); - return GST_FLOW_ERROR; - } - - if (!gst_buffer_map(*buffer, &dest_info, GST_MAP_WRITE)) { - GST_ERROR("Failed to map the buffer"); - for (int i = 0; i < anc_count; i++) free(anc_data[i]); - return GST_FLOW_ERROR; - } - - guint fill_size = 0; - for (int i = 0; i < anc_count; i++) { - fill_size += - gst_buffer_fill(*buffer, fill_size, anc_data[i], anc_data_count[i] + meta_offset); - free(anc_data[i]); - } - - gst_buffer_unmap(*buffer, &dest_info); - - if (fill_size != buffer_size) { - GST_ERROR("Failed to fill buffer (buffer size = %d, fill size = %d)", buffer_size, - fill_size); - ; - } - - return GST_FLOW_OK; -} - -static GstFlowReturn gst_mtl_st40_rx_create(GstBaseSrc* basesrc, guint64 offset, - guint length, GstBuffer** buffer) { - Gst_Mtl_St40_Rx* src = GST_MTL_ST40_RX(basesrc); - void *mbuf, *usrptr; - guint16 size; - gint ret; - - GST_OBJECT_LOCK(src); - - /* get the mbuff */ - mbuf = st40_rx_get_mbuf(src->rx_handle, &usrptr, &size); - if (!mbuf) { - mbuf = gst_mtl_st40_rx_get_mbuf_with_timeout(src, src->rx_handle, &usrptr, &size); - } - - if (!mbuf) { - GST_OBJECT_UNLOCK(src); - return GST_FLOW_EOS; - } - - if (size == 0) { - GST_ERROR("No ancillary data received"); - GST_OBJECT_UNLOCK(src); - return GST_FLOW_ERROR; - } - - ret = gst_mtl_st40_rx_fill_buffer(src, buffer, usrptr); - - if (ret != GST_FLOW_OK) { - GST_ERROR("Failed to fill buffer"); - } - - st40_rx_put_mbuf(src->rx_handle, mbuf); - GST_OBJECT_UNLOCK(src); - return ret; -} - -static void gst_mtl_st40_rx_finalize(GObject* object) { - Gst_Mtl_St40_Rx* src = GST_MTL_ST40_RX(object); - - if (src->rx_handle) { - if (st40_rx_free(src->rx_handle)) { - GST_ERROR("Failed to free rx handle"); - } - } - - pthread_mutex_destroy(&src->mbuff_mutex); - pthread_cond_destroy(&src->mbuff_cond); - - if (src->mtl_lib_handle) { - if (gst_mtl_common_deinit_handle(&src->mtl_lib_handle)) { - GST_ERROR("Failed to uninitialize MTL library"); - } - } -} - -static gboolean plugin_init(GstPlugin* mtl_st40_rx) { - return gst_element_register(mtl_st40_rx, "mtl_st40_rx", GST_RANK_SECONDARY, - GST_TYPE_MTL_ST40_RX); -} - -GST_PLUGIN_DEFINE(GST_VERSION_MAJOR, GST_VERSION_MINOR, mtl_st40_rx, - "software-based solution designed for high-throughput transmission", - plugin_init, PACKAGE_VERSION, GST_LICENSE, GST_PACKAGE_NAME, - GST_PACKAGE_ORIGIN); diff --git a/ecosystem/gstreamer_plugin/gst_mtl_st40_rx.h b/ecosystem/gstreamer_plugin/gst_mtl_st40_rx.h deleted file mode 100644 index a5cad659f..000000000 --- a/ecosystem/gstreamer_plugin/gst_mtl_st40_rx.h +++ /dev/null @@ -1,94 +0,0 @@ -/* - * GStreamer - * Copyright (C) 2005 Thomas Vander Stichele - * Copyright (C) 2005 Ronald S. Bultje - * Copyright (C) 2020 Niels De Graef - * Copyright (C) 2024 Intel Corporation - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - * - * Alternatively, the contents of this file may be used under the - * GNU Lesser General Public License Version 2.1 (the "LGPL"), in - * which case the following provisions apply instead of the ones - * mentioned above: - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 59 Temple Place - Suite 330, - * Boston, MA 02111-1307, USA. - */ - -#ifndef __GST_MTL_ST40_RX_H__ -#define __GST_MTL_ST40_RX_H__ - -#define IS_POWER_OF_2(x) (((x) & ((x)-1)) == 0) -#define ST40_RFC8331_PAYLOAD_MAX_ANCILLARY_COUNT 20 -#define USER_DATA_WORD_BIT_SIZE 10 -#define BYTE_SIZE 8 -#define OFFSET_BIT 32 -#define WORD_10_BIT_ALIGN ((OFFSET_BIT / USER_DATA_WORD_BIT_SIZE) + 1) - -/** - * ST40_SIZE_OF_PAYLOAD_METADATA: - * Defines the size (in bytes) of the payload metadata for ST40 streams. - * This value is set to 8 to accommodate the following fields: - * c | line_number | horizontal_offset | s | stream_num | did | sdid | - * data_count - */ -#define ST40_BYTE_SIZE_OF_PAYLOAD_METADATA 8 - -#include - -#include "gst_mtl_common.h" - -G_BEGIN_DECLS - -#define GST_TYPE_MTL_ST40_RX (gst_mtl_st40_rx_get_type()) -G_DECLARE_FINAL_TYPE(Gst_Mtl_St40_Rx, gst_mtl_st40_rx, GST, MTL_ST40_RX, GstBaseSrc) - -struct _Gst_Mtl_St40_Rx { - GstBaseSrc element; - - /*< private >*/ - pthread_mutex_t mbuff_mutex; - pthread_cond_t mbuff_cond; - mtl_handle mtl_lib_handle; - st40_rx_handle rx_handle; - - /* arguments */ - GeneralArgs generalArgs; /* imtl initialization arguments */ - SessionPortArgs portArgs; /* imtl session device */ - guint timeout_mbuf_get_seconds; - guint16 mbuff_size; - gboolean include_metadata_in_buffer; -}; - -G_END_DECLS - -#endif /* __GST_MTL_ST40_RX_H__ */ diff --git a/ecosystem/gstreamer_plugin/gst_mtl_st40p_tx.c b/ecosystem/gstreamer_plugin/gst_mtl_st40p_tx.c index 370a8d9a0..b3b07545d 100644 --- a/ecosystem/gstreamer_plugin/gst_mtl_st40p_tx.c +++ b/ecosystem/gstreamer_plugin/gst_mtl_st40p_tx.c @@ -651,10 +651,8 @@ static GstFlowReturn gst_mtl_st40p_tx_parse_8331_anc_words( payload_cursor = (uint8_t*)map_info.data + (buffer_size - bytes_left_to_process); rfc8331_meta.headers[i] = (struct st40_rfc8331_payload_hdr*)payload_cursor; - payload_header.swapped_first_hdr_chunk = - ntohl(rfc8331_meta.headers[i]->swapped_first_hdr_chunk); - payload_header.swapped_second_hdr_chunk = - ntohl(rfc8331_meta.headers[i]->swapped_second_hdr_chunk); + payload_header = *rfc8331_meta.headers[i]; + st40_rfc8331_payload_hdr_bswap(&payload_header); payload_cursor = (uint8_t*)&rfc8331_meta.headers[i]->swapped_second_hdr_chunk; /* diff --git a/ecosystem/gstreamer_plugin/meson.build b/ecosystem/gstreamer_plugin/meson.build index 422eabea1..c48cb8e2d 100644 --- a/ecosystem/gstreamer_plugin/meson.build +++ b/ecosystem/gstreamer_plugin/meson.build @@ -122,20 +122,6 @@ gstmtl_st30p_tx = library('gstmtl_st30p_tx', c_args: plugin_c_args ) -# mtl_st40_rx Plugin -gst_mtl_st40_rx_sources = [ - 'gst_mtl_st40_rx.c' -] - -gst_mtl_st40_rx = library('gstmtl_st40_rx', - gst_mtl_st40_rx_sources, - dependencies : [gst_dep, gstbase_dep, gstreamer_audio_dep, mtl_dep, gst_mtl_common_dep], - install : true, - install_dir : plugins_install_dir, - include_directories: inc_dirs, - c_args: plugin_c_args -) - # mtl_st40p_tx Plugin gst_mtl_st40p_tx_sources = [ 'gst_mtl_st40p_tx.c' diff --git a/include/st20_api.h b/include/st20_api.h index e8febf7ee..c41ffcb88 100644 --- a/include/st20_api.h +++ b/include/st20_api.h @@ -1242,7 +1242,7 @@ struct st20_tx_ops { */ uint16_t rtp_pkt_size; /** - * Optional for ST20_TYPE_RTP_LEVEL. + * Mandatory for ST20_TYPE_RTP_LEVEL. * The callback when lib finish the sending of one rtp packet. * And only non-block method can be used within this callback as it run from lcore * tasklet routine. @@ -1366,7 +1366,7 @@ struct st22_tx_ops { */ uint16_t rtp_pkt_size; /** - * Optional for ST22_TYPE_RTP_LEVEL. + * Mandatory for ST22_TYPE_RTP_LEVEL. * The callback when lib finish the sending of one rtp packet. * And only non-block method can be used within this callback as it run from lcore * tasklet routine. @@ -1585,7 +1585,7 @@ struct st20_rx_ops { /** Mandatory for ST20_TYPE_RTP_LEVEL. rtp ring queue size, must be power of 2 */ uint32_t rtp_ring_size; /** - * Optional for ST20_TYPE_RTP_LEVEL. The callback when lib receive one rtp packet. + * Mandatory for ST20_TYPE_RTP_LEVEL. The callback when lib receive one rtp packet. * And only non-block method can be used in this callback as it run from lcore tasklet * routine. */ @@ -1690,7 +1690,7 @@ struct st22_rx_ops { /** Mandatory for ST22_TYPE_RTP_LEVEL. rtp ring queue size, must be power of 2 */ uint32_t rtp_ring_size; /** - * Optional for ST22_TYPE_RTP_LEVEL. The callback when lib receive one rtp packet. + * Mandatory for ST22_TYPE_RTP_LEVEL. The callback when lib receive one rtp packet. * And only non-block method can be used in this callback as it run from lcore tasklet * routine. */ diff --git a/include/st30_api.h b/include/st30_api.h index 71e428c24..3a7979fff 100644 --- a/include/st30_api.h +++ b/include/st30_api.h @@ -423,9 +423,9 @@ struct st30_tx_ops { /** Mandatory for ST30_TYPE_RTP_LEVEL. rtp ring queue size, must be power of 2 */ uint32_t rtp_ring_size; /** - * Optional for ST30_TYPE_RTP_LEVEL. the callback when lib finish the sending of one rtp - * packet. And only non-block method can be used in this callback as it run from lcore - * tasklet routine. + * Mandatory for ST30_TYPE_RTP_LEVEL. the callback when lib finish the sending of one + * rtp packet. And only non-block method can be used in this callback as it run from + * lcore tasklet routine. */ int (*notify_rtp_done)(void* priv); /** Optional for ST30_TX_PACING_WAY_RL, the required accuracy for warmup check point */ @@ -518,7 +518,7 @@ struct st30_rx_ops { /** Mandatory for ST30_TYPE_RTP_LEVEL. rtp ring queue size, must be power of 2 */ uint32_t rtp_ring_size; /** - * Optional for ST20_TYPE_RTP_LEVEL. The callback when lib receive one rtp packet. + * Mandatory for ST30_TYPE_RTP_LEVEL. The callback when lib receive one rtp packet. * And only non-block method can be used in this callback as it run from lcore tasklet * routine. */ diff --git a/include/st40_api.h b/include/st40_api.h index 7a215529f..d6fe47328 100644 --- a/include/st40_api.h +++ b/include/st40_api.h @@ -402,13 +402,63 @@ struct st40_tx_ops { /** Mandatory for ST40_TYPE_RTP_LEVEL. rtp ring queue size, must be power of 2 */ uint32_t rtp_ring_size; /** - * Optional for ST40_TYPE_RTP_LEVEL. callback when lib finish the sending of one rtp + * Mandatory for ST40_TYPE_RTP_LEVEL. callback when lib finish the sending of one rtp * packet, And only non-block method can be used in this callback as it run from lcore * tasklet routine. */ int (*notify_rtp_done)(void* priv); }; +/** + * Frame meta data of st2110-40(ancillary) rx streaming. + * + * Mirrors `struct st30_rx_frame_meta` so a future merge of ST30/ST40 RX + * transport code is straightforward. Filled by the transport assembler + * before invoking `notify_frame_ready`. + */ +struct st40_rx_frame_meta { + /** Frame timestamp format */ + enum st10_timestamp_fmt tfmt; + /** Frame timestamp value (media clock) */ + uint64_t timestamp; + /** TAI timestamp captured for the first received RTP packet of this frame */ + uint64_t timestamp_first_pkt; + /** Timestamp value in the RTP header */ + uint32_t rtp_timestamp; + + /** Total RTP packets used to assemble this frame (excludes redundant duplicates) */ + uint32_t pkts_total; + /** Per-session-port RTP packets received for this frame */ + uint32_t pkts_recv[MTL_SESSION_PORT_MAX]; + + /** Per-session-port intra-frame seq gaps (after the per-port redundancy filter) */ + uint32_t port_seq_lost[MTL_SESSION_PORT_MAX]; + /** Per-session-port intra-frame seq discontinuity flag */ + bool port_seq_discont[MTL_SESSION_PORT_MAX]; + + /** Session-level intra-frame seq gaps after redundancy bitmap correction */ + uint32_t seq_lost; + /** Session-level intra-frame seq discontinuity flag */ + bool seq_discont; + + /** True when an RTP marker bit was observed on any packet in this frame */ + bool rtp_marker; + /** True if the RTP F bits identified this frame as interlaced */ + bool interlaced; + /** True when the frame is the second field of an interlaced pair (F=0b11) */ + bool second_field; + + /** Number of `struct st40_meta` entries written into the meta array */ + uint32_t meta_num; + /** Pointer to the meta array, sized ST40_MAX_META, owned by transport */ + struct st40_meta* meta; + /** Bytes written into the UDW buffer */ + size_t udw_buffer_fill; + + /** Frame status (complete / corrupted) */ + enum st_frame_status status; +}; + /** * The structure describing how to create a rx st2110-40(ancillary) session. * Include the PCIE port and other required info @@ -428,6 +478,9 @@ struct st40_rx_ops { /** Mandatory. UDP dest port number */ uint16_t udp_port[MTL_SESSION_PORT_MAX]; + /** Mandatory. Session streaming type, frame or RTP. */ + enum st40_type type; + /** Mandatory. 7 bits payload type define in RFC3550. Zero means disable the * payload_type check on the RX pkt path */ uint8_t payload_type; @@ -448,13 +501,38 @@ struct st40_rx_ops { /** Optional. see ST40_RX_FLAG_* for possible flags */ uint32_t flags; - /** Mandatory. rtp ring queue size, must be power of 2 */ + /** Mandatory for ST40_TYPE_RTP_LEVEL. rtp ring queue size, must be power of 2 */ uint32_t rtp_ring_size; /** - * Optional. the callback when lib finish the sending of one rtp packet. And only - * non-block method can be used in this callback as it run from lcore tasklet routine. + * Mandatory for ST40_TYPE_RTP_LEVEL. The callback when lib receive one rtp packet. + * And only non-block method can be used in this callback as it run from lcore tasklet + * routine. */ int (*notify_rtp_ready)(void* priv); + + /** + * Mandatory for ST40_TYPE_FRAME_LEVEL. Number of frame buffers in the + * transport-owned pool. + */ + uint16_t framebuff_cnt; + /** + * Mandatory for ST40_TYPE_FRAME_LEVEL. Size in bytes of the User Data Word + * buffer attached to each frame slot. Sized by the consumer to fit the + * largest expected ANC payload. + */ + uint32_t framebuff_size; + /** + * Mandatory for ST40_TYPE_FRAME_LEVEL. Callback when lib finishes assembling + * one full frame. Frame ownership is transferred to the app; the app must + * call `st40_rx_put_framebuff` once it has consumed the frame. + * frame: pointer to the UDW buffer of the frame. + * meta: transport-filled meta (timestamps, packet counts, seq stats, etc). + * return: + * - 0: app accepted the frame. + * - <0: lib will reclaim the frame slot immediately. + * Must be non-blocking — runs on the lcore tasklet. + */ + int (*notify_frame_ready)(void* priv, void* frame, struct st40_rx_frame_meta* meta); }; /** @@ -687,6 +765,20 @@ void* st40_rx_get_mbuf(st40_rx_handle handle, void** usrptr, uint16_t* len); */ void st40_rx_put_mbuf(st40_rx_handle handle, void* mbuf); +/** + * Return a frame buffer obtained via the `notify_frame_ready` callback to the + * rx st2110-40(ancillary) session. For ST40_TYPE_FRAME_LEVEL. + * + * @param handle + * The handle to the rx st2110-40(ancillary) session. + * @param frame + * The UDW buffer pointer that was passed to `notify_frame_ready`. + * @return + * - 0: Success. + * - <0: Error code. + */ +int st40_rx_put_framebuff(st40_rx_handle handle, void* frame); + /** * Get the queue meta attached to rx st2110-40(ancillary) session. * @@ -710,7 +802,7 @@ int st40_rx_get_queue_meta(st40_rx_handle handle, struct st_queue_meta* meta); * @return * - udw */ -uint16_t st40_get_udw(uint32_t idx, uint8_t* data); +uint16_t st40_get_udw(uint32_t idx, const uint8_t* data); /** * Set udw from for st2110-40(ancillary) payload. @@ -734,7 +826,71 @@ void st40_set_udw(uint32_t idx, uint16_t udw, uint8_t* data); * @return * - checksum */ -uint16_t st40_calc_checksum(uint32_t data_num, uint8_t* data); +uint16_t st40_calc_checksum(uint32_t data_num, const uint8_t* data); + +/** + * Wire-aligned byte size of one RFC 8331 ANC data packet inside an RTP + * payload (4-byte payload header + 10-bit-packed DID/SDID/DC/UDW[]/checksum, + * ceil to whole bytes, then rounded up to a 4-byte boundary). + * + * @param udw_size + * User Data Word count (0..255, the 8-bit DataCount field). + * @return byte count to advance per ANC packet on the wire. + */ +uint32_t st40_rfc8331_payload_bytes(uint16_t udw_size); + +/** + * Byte-swap the first header chunk of an RFC 8331 RTP header in place + * between host and network order. + */ +void st40_rfc8331_rtp_hdr_bswap(struct st40_rfc8331_rtp_hdr* hdr); + +/** + * Byte-swap both header chunks of an RFC 8331 payload header in place + * between host and network order. + */ +void st40_rfc8331_payload_hdr_bswap(struct st40_rfc8331_payload_hdr* hdr); + +/** + * Result of st40_rfc8331_decode_packet(). + */ +enum st40_rfc8331_decode_result { + ST40_RFC8331_DECODE_OK = 0, + ST40_RFC8331_DECODE_SHORT_BUFFER, + ST40_RFC8331_DECODE_PARITY_FAIL, + ST40_RFC8331_DECODE_CHECKSUM_FAIL, +}; + +/** + * Decode one wire-order RFC 8331 ANC sub-packet from @p buf (capacity + * @p room), write parsed meta to @p meta and up to @p udw_cap stripped + * UDW bytes (low 8 bits per word) to @p udw_out. + * + * On success @p *consumed is set to the number of bytes occupied on the + * wire (post 4-byte alignment) so the caller can advance to the next + * sub-packet. @p udw_out may be NULL with @p udw_cap == 0 when the + * caller only needs meta and size. + */ +enum st40_rfc8331_decode_result st40_rfc8331_decode_packet( + const uint8_t* buf, uint32_t room, struct st40_meta* meta, uint8_t* udw_out, + uint32_t udw_cap, uint32_t* consumed); + +/** + * Encode one ANC sub-packet at @p buf (capacity @p room) from @p meta and + * @p udw_in (meta->udw_size bytes; may be NULL when meta->udw_size == 0). + * On success writes wire-order bytes and sets @p *written to the bytes + * occupied on the wire. + * + * Used by in-tree TX (st_tx_ancillary_session.c) as well as external + * callers/plugins. + * + * @return + * - 0 on success + * - -ENOSPC if @p room is too small for the packet + * - -EINVAL if @p meta->udw_size exceeds the wire field's 8-bit range (255) + */ +int st40_rfc8331_encode_packet(uint8_t* buf, uint32_t room, const struct st40_meta* meta, + const uint8_t* udw_in, uint32_t* written); /** * Add parity from st2110-40(ancillary) payload. diff --git a/include/st41_api.h b/include/st41_api.h index b8bf0d9b8..eee590d3d 100644 --- a/include/st41_api.h +++ b/include/st41_api.h @@ -218,7 +218,7 @@ struct st41_tx_ops { /** Mandatory for ST41_TYPE_RTP_LEVEL. rtp ring queue size, must be power of 2 */ uint32_t rtp_ring_size; /** - * Optional for ST41_TYPE_RTP_LEVEL. callback when lib finish the sending of one rtp + * Mandatory for ST41_TYPE_RTP_LEVEL. callback when lib finish the sending of one rtp * packet, And only non-block method can be used in this callback as it run from lcore * tasklet routine. */ @@ -271,7 +271,7 @@ struct st41_rx_ops { /** Mandatory. rtp ring queue size, must be power of 2 */ uint32_t rtp_ring_size; /** - * Optional. the callback when lib finish the sending of one rtp packet. And only + * Mandatory. the callback when lib receive one rtp packet. And only * non-block method can be used in this callback as it run from lcore tasklet routine. */ int (*notify_rtp_ready)(void* priv); diff --git a/lib/src/st2110/pipeline/st40_pipeline_rx.c b/lib/src/st2110/pipeline/st40_pipeline_rx.c index eb3c76127..ef6ad9c60 100644 --- a/lib/src/st2110/pipeline/st40_pipeline_rx.c +++ b/lib/src/st2110/pipeline/st40_pipeline_rx.c @@ -9,7 +9,9 @@ #include "../../mt_stat.h" static const char* st40p_rx_frame_stat_name[ST40P_RX_FRAME_STATUS_MAX] = { - "free", "receiving", "pending", "ready", "in_user", + "free", + "ready", + "in_user", }; static const char* rx_st40p_stat_name(enum st40p_rx_frame_status stat) { @@ -66,324 +68,64 @@ static struct st40p_rx_frame* rx_st40p_next_available( return NULL; } -static int rx_st40p_rtp_ready(void* priv) { +/* FRAME_LEVEL transport delivery callback. Transport hands us a fully + * assembled frame (UDW buffer + meta). We claim a FREE pipeline framebuff, + * point it at the transport-owned UDW buffer (zero-copy) and copy meta into + * the pipeline-owned meta[] array. */ +static int rx_st40p_frame_ready(void* priv, void* addr, struct st40_rx_frame_meta* meta) { struct st40p_rx_ctx* ctx = priv; struct st40p_rx_frame* framebuff; - void* mbuf; - void* usrptr = NULL; - uint16_t len = 0; - struct st40_rfc8331_rtp_hdr* hdr; - struct st40_frame_info* frame_info = NULL; - uint32_t anc_count; - uint8_t* payload; - struct st40_rfc8331_payload_hdr* payload_hdr; - uint32_t payload_offset = 0; - bool notify_frame = false; - struct st40p_rx_frame* done_frames[2] = {0}; - struct st40_frame_info* done_infos[2] = {0}; - int done_count = 0; - int ret = 0; - - if (!ctx->ready) return -EBUSY; /* not ready */ - - /* get mbuf from transport */ - mbuf = st40_rx_get_mbuf(ctx->transport, &usrptr, &len); - if (!mbuf) return -EBUSY; - - struct rte_mbuf* pkt = mbuf; - uint16_t pkt_port_id = pkt->port; - int s_port = -1; - enum mtl_port phy_port = MTL_PORT_MAX; - for (int i = 0; i < MTL_SESSION_PORT_MAX; i++) { - if (pkt_port_id == ctx->port_id[i]) { - s_port = i; - phy_port = ctx->port_map[i]; - break; - } - } - if (s_port < 0 || phy_port >= MTL_PORT_MAX) { - warn("%s(%d), drop pkt: unmapped port_id %u\n", __func__, ctx->idx, pkt_port_id); - st40_rx_put_mbuf(ctx->transport, mbuf); - return -EIO; - } - uint64_t receive_timestamp = mt_mbuf_time_stamp(ctx->impl, pkt, phy_port); - - uint32_t hdr_bytes = sizeof(struct st40_rfc8331_rtp_hdr); - if (len < hdr_bytes) { - warn("%s(%d), RTP packet too small (%u < %u)\n", __func__, ctx->idx, len, hdr_bytes); - st40_rx_put_mbuf(ctx->transport, mbuf); - return -EIO; - } + struct st40_frame_info* frame_info; - hdr = (struct st40_rfc8331_rtp_hdr*)usrptr; - anc_count = hdr->first_hdr_chunk.anc_count; - uint32_t rtp_timestamp = ntohl(hdr->base.tmstamp); - uint16_t seq_number = ntohs(hdr->base.seq_number); + if (!ctx->ready) return -EBUSY; mt_pthread_mutex_lock(&ctx->lock); - - /* Route late packet to pending frame if it matches the pending timestamp */ - if (ctx->pending_frame && rtp_timestamp == ctx->pending_rtp_timestamp) { - framebuff = ctx->pending_frame; - frame_info = &framebuff->frame_info; - if (!frame_info->receive_timestamp || - (frame_info->receive_timestamp > receive_timestamp)) - frame_info->receive_timestamp = receive_timestamp; - } else { - /* Handle timestamp change on inflight frame */ - if (ctx->inflight_frame && ctx->inflight_rtp_timestamp != rtp_timestamp) { - if (ctx->ops.port.num_port > 1) { - /* Multi-port: inflight → PENDING (wait for late marker from redundant port) */ - if (ctx->pending_frame) { - /* Force-deliver existing pending frame first */ - ctx->pending_frame->stat = ST40P_RX_FRAME_READY; - notify_frame = true; - done_frames[done_count] = ctx->pending_frame; - done_infos[done_count] = &ctx->pending_frame->frame_info; - done_count++; - } - ctx->inflight_frame->stat = ST40P_RX_FRAME_PENDING; - ctx->framebuff_producer_idx = rx_st40p_next_idx(ctx, ctx->inflight_frame->idx); - ctx->pending_frame = ctx->inflight_frame; - ctx->pending_rtp_timestamp = ctx->inflight_rtp_timestamp; - } else { - /* Single-port: no redundant port → directly READY */ - ctx->inflight_frame->stat = ST40P_RX_FRAME_READY; - ctx->framebuff_producer_idx = rx_st40p_next_idx(ctx, ctx->inflight_frame->idx); - notify_frame = true; - done_frames[done_count] = ctx->inflight_frame; - done_infos[done_count] = &ctx->inflight_frame->frame_info; - done_count++; - } - ctx->inflight_frame = NULL; - } - - if (!ctx->inflight_frame) { - framebuff = - rx_st40p_next_available(ctx, ctx->framebuff_producer_idx, ST40P_RX_FRAME_FREE); - - if (!framebuff) { - ctx->stat_busy++; - /* relaxed atomic: written under ctx->lock from tasklet, read from any - * thread via st40p_rx_get_session_stats() without locking. */ - __atomic_fetch_add(&ctx->stat_frames_dropped, 1, __ATOMIC_RELAXED); - ret = -EBUSY; - goto out; - } - - framebuff->stat = ST40P_RX_FRAME_RECEIVING; - frame_info = &framebuff->frame_info; - frame_info->meta_num = 0; - frame_info->udw_buffer_fill = 0; - frame_info->pkts_total = 0; - frame_info->pkts_recv[MTL_SESSION_PORT_P] = 0; - frame_info->pkts_recv[MTL_SESSION_PORT_R] = 0; - frame_info->seq_discont = false; - frame_info->seq_lost = 0; - frame_info->rtp_marker = false; - frame_info->receive_timestamp = receive_timestamp; - frame_info->tfmt = ST10_TIMESTAMP_FMT_MEDIA_CLK; - frame_info->rtp_timestamp = rtp_timestamp; - frame_info->timestamp = rtp_timestamp; - frame_info->epoch = 0; - uint8_t f_bits = hdr->first_hdr_chunk.f & 0x3; - frame_info->interlaced = (f_bits & 0x2) ? true : false; - frame_info->second_field = frame_info->interlaced && (f_bits & 0x1); - ctx->inflight_frame = framebuff; - ctx->inflight_rtp_timestamp = rtp_timestamp; - } else { - framebuff = ctx->inflight_frame; - frame_info = &framebuff->frame_info; - if (!frame_info->receive_timestamp || - (frame_info->receive_timestamp > receive_timestamp)) - frame_info->receive_timestamp = receive_timestamp; - - /* Update field metadata if a later packet carries interlace info */ - uint8_t pkt_field = hdr->first_hdr_chunk.f & 0x3; - bool pkt_interlaced = (pkt_field & 0x2) ? true : false; - bool pkt_second_field = pkt_interlaced && (pkt_field & 0x1); - if ((frame_info->interlaced != pkt_interlaced) || - (frame_info->second_field != pkt_second_field)) { - frame_info->interlaced = pkt_interlaced; - frame_info->second_field = pkt_second_field; - } - } - } - - /* per-port sequence tracking */ - if (ctx->last_seq_valid[s_port]) { - uint16_t expected = (uint16_t)(ctx->last_seq[s_port] + 1); - if (expected != seq_number) { - frame_info->port_seq_discont[s_port] = true; - if (mt_seq16_greater(seq_number, expected)) - frame_info->port_seq_lost[s_port] += (uint16_t)(seq_number - expected); - } - } - ctx->last_seq[s_port] = seq_number; - ctx->last_seq_valid[s_port] = true; - - /* session-level sequence tracking (merged across all ports) */ - if (ctx->session_last_seq_valid) { - uint16_t expected = (uint16_t)(ctx->session_last_seq + 1); - if (expected != seq_number && mt_seq16_greater(seq_number, ctx->session_last_seq)) { - frame_info->seq_discont = true; - frame_info->seq_lost += (uint16_t)(seq_number - expected); - } - } - if (!ctx->session_last_seq_valid || - mt_seq16_greater(seq_number, ctx->session_last_seq)) { - ctx->session_last_seq = seq_number; - ctx->session_last_seq_valid = true; - } - - frame_info->pkts_total++; - frame_info->pkts_recv[s_port]++; - - /* parse RTP packet and copy metadata */ - payload = (uint8_t*)(hdr + 1); - uint32_t payload_room = (len > sizeof(*hdr)) ? (len - sizeof(*hdr)) : 0; - - for (uint32_t anc_idx = 0; anc_idx < anc_count; anc_idx++) { - if (frame_info->meta_num >= ST40_MAX_META) { - warn("%s(%d), meta slots exhausted after %u packets\n", __func__, ctx->idx, - frame_info->meta_num); - break; - } - - if (payload_offset + sizeof(struct st40_rfc8331_payload_hdr) > payload_room) { - warn("%s(%d), payload offset exceeds RTP payload (offset=%u, room=%u)\n", __func__, - ctx->idx, payload_offset, payload_room); - break; - } - - payload_hdr = (struct st40_rfc8331_payload_hdr*)(payload + payload_offset); - - struct st40_rfc8331_payload_hdr hdr_local = {0}; - hdr_local.swapped_first_hdr_chunk = ntohl(payload_hdr->swapped_first_hdr_chunk); - hdr_local.swapped_second_hdr_chunk = ntohl(payload_hdr->swapped_second_hdr_chunk); - - uint16_t udw_words = hdr_local.second_hdr_chunk.data_count & 0xFF; - struct st40_meta* meta_entry = &frame_info->meta[frame_info->meta_num]; - meta_entry->c = hdr_local.first_hdr_chunk.c; - meta_entry->line_number = hdr_local.first_hdr_chunk.line_number; - meta_entry->hori_offset = hdr_local.first_hdr_chunk.horizontal_offset; - meta_entry->s = hdr_local.first_hdr_chunk.s; - meta_entry->stream_num = hdr_local.first_hdr_chunk.stream_num; - meta_entry->did = hdr_local.second_hdr_chunk.did & 0xFF; - meta_entry->sdid = hdr_local.second_hdr_chunk.sdid & 0xFF; - meta_entry->udw_size = udw_words; - meta_entry->udw_offset = frame_info->udw_buffer_fill; - - uint32_t total_bits = (3 + udw_words + 1) * 10; - /* SMPTE ST 2110-40 / RFC 8331 packs the 10-bit fields (DID, SDID, DC, - * UDW[], checksum) bit-contiguously. Round the total bit count up to - * whole bytes, then 4-byte align per the RFC. */ - uint32_t total_size = (total_bits + 7) / 8; - uint32_t total_size_aligned = (total_size + 3) & ~0x3U; - uint32_t anc_packet_bytes = - sizeof(struct st40_rfc8331_payload_hdr) - 4 + total_size_aligned; - if (payload_offset + anc_packet_bytes > payload_room) { - warn("%s(%d), ANC packet bytes exceed payload (offset=%u, size=%u, room=%u)\n", - __func__, ctx->idx, payload_offset, anc_packet_bytes, payload_room); - break; - } - - /* If this is an empty ANC frame (udw_words == 0), still preserve and count it */ - bool meta_valid = true; - if (udw_words == 0) { - /* Accept and preserve empty ANC frame as valid */ - meta_valid = true; - } else { - /* Parse and validate UDW as before */ - uint8_t* udw_src = (uint8_t*)&payload_hdr->second_hdr_chunk; - uint32_t original_fill = frame_info->udw_buffer_fill; - for (uint16_t udw_idx = 0; udw_idx < udw_words; udw_idx++) { - uint16_t udw = st40_get_udw(udw_idx + 3, udw_src); - if (!st40_check_parity_bits(udw)) { - warn("%s(%d), UDW parity failure packet %u word %u\n", __func__, ctx->idx, - anc_idx, udw_idx); - meta_valid = false; - break; - } - if (frame_info->udw_buffer_fill >= frame_info->udw_buffer_size) { - warn("%s(%d), UDW buffer overflow for packet %u\n", __func__, ctx->idx, - anc_idx); - meta_valid = false; - break; - } - frame_info->udw_buff_addr[frame_info->udw_buffer_fill++] = (uint8_t)(udw & 0xFF); - } - if (meta_valid) { - uint16_t checksum_udw = st40_get_udw(udw_words + 3, udw_src); - uint16_t checksum_calc = st40_calc_checksum(3 + udw_words, udw_src); - if (checksum_udw != checksum_calc) { - warn("%s(%d), checksum mismatch packet %u (0x%03x != 0x%03x)\n", __func__, - ctx->idx, anc_idx, checksum_udw, checksum_calc); - meta_valid = false; - } - } - if (!meta_valid) { - frame_info->udw_buffer_fill = original_fill; - break; - } - } - - frame_info->meta_num++; - payload_offset += anc_packet_bytes; - } - - if (hdr->base.marker) { - frame_info->rtp_marker = true; - if (framebuff == ctx->pending_frame) { - /* Late marker resolves PENDING → READY */ - framebuff->stat = ST40P_RX_FRAME_READY; - ctx->pending_frame = NULL; - } else { - /* Normal inflight → READY */ - framebuff->stat = ST40P_RX_FRAME_READY; - ctx->framebuff_producer_idx = rx_st40p_next_idx(ctx, framebuff->idx); - ctx->inflight_frame = NULL; - } - notify_frame = true; - done_frames[done_count] = framebuff; - done_infos[done_count] = frame_info; - done_count++; - } - - /* Assign frame status while still under lock — app thread may read status - * after get_frame() returns, so the write must be visible before unlock. */ - for (int n = 0; n < done_count; n++) { - struct st40_frame_info* done_info = done_infos[n]; - if (done_info) { - done_info->status = - done_info->seq_discont ? ST_FRAME_STATUS_CORRUPTED : ST_FRAME_STATUS_COMPLETE; - __atomic_fetch_add(&ctx->stat_frames_received, 1, __ATOMIC_RELAXED); - if (done_info->seq_discont) - __atomic_fetch_add(&ctx->stat_frames_corrupted, 1, __ATOMIC_RELAXED); - } + framebuff = + rx_st40p_next_available(ctx, ctx->framebuff_producer_idx, ST40P_RX_FRAME_FREE); + if (!framebuff) { + ctx->stat_busy++; + __atomic_fetch_add(&ctx->stat_frames_dropped, 1, __ATOMIC_RELAXED); + mt_pthread_mutex_unlock(&ctx->lock); + /* returning <0 makes the transport reclaim the slot to FREE */ + return -EBUSY; } -out: + frame_info = &framebuff->frame_info; + frame_info->udw_buff_addr = (uint8_t*)addr; + frame_info->udw_buffer_fill = meta->udw_buffer_fill; + frame_info->meta_num = meta->meta_num; + if (meta->meta_num && meta->meta) { + uint32_t copy = meta->meta_num > ST40_MAX_META ? ST40_MAX_META : meta->meta_num; + memcpy(frame_info->meta, meta->meta, sizeof(struct st40_meta) * copy); + } + frame_info->pkts_total = meta->pkts_total; + for (int i = 0; i < MTL_SESSION_PORT_MAX; i++) { + frame_info->pkts_recv[i] = meta->pkts_recv[i]; + frame_info->port_seq_lost[i] = meta->port_seq_lost[i]; + frame_info->port_seq_discont[i] = meta->port_seq_discont[i]; + } + frame_info->seq_lost = meta->seq_lost; + frame_info->seq_discont = meta->seq_discont; + frame_info->rtp_marker = meta->rtp_marker; + frame_info->interlaced = meta->interlaced; + frame_info->second_field = meta->second_field; + frame_info->tfmt = meta->tfmt; + frame_info->rtp_timestamp = meta->rtp_timestamp; + frame_info->timestamp = meta->timestamp; + frame_info->receive_timestamp = meta->timestamp_first_pkt; + frame_info->epoch = 0; + frame_info->status = meta->status; + + framebuff->stat = ST40P_RX_FRAME_READY; + ctx->framebuff_producer_idx = rx_st40p_next_idx(ctx, framebuff->idx); + __atomic_fetch_add(&ctx->stat_frames_received, 1, __ATOMIC_RELAXED); + if (frame_info->seq_discont) + __atomic_fetch_add(&ctx->stat_frames_corrupted, 1, __ATOMIC_RELAXED); mt_pthread_mutex_unlock(&ctx->lock); - /* return mbuf to transport */ - st40_rx_put_mbuf(ctx->transport, mbuf); - - if (notify_frame) { - for (int n = 0; n < done_count; n++) { - struct st40p_rx_frame* report_frame = done_frames[n]; - struct st40_frame_info* report_info = done_infos[n]; - if (!report_frame || !report_info) continue; - dbg("%s(%d), frame %u succ, meta_num %u\n", __func__, ctx->idx, report_frame->idx, - report_info->meta_num); - /* notify app to a ready frame */ - rx_st40p_notify_frame_available(ctx); - MT_USDT_ST40P_RX_FRAME_AVAILABLE(ctx->idx, report_frame->idx, - report_info->meta_num); - } - } - - return ret; + rx_st40p_notify_frame_available(ctx); + MT_USDT_ST40P_RX_FRAME_AVAILABLE(ctx->idx, framebuff->idx, frame_info->meta_num); + return 0; } static int rx_st40p_create_transport(struct mtl_main_impl* impl, struct st40p_rx_ctx* ctx, @@ -410,8 +152,12 @@ static int rx_st40p_create_transport(struct mtl_main_impl* impl, struct st40p_rx ctx->port_id[i] = mt_port_id(impl, phy); } - ops_rx.rtp_ring_size = ops->rtp_ring_size; - ops_rx.notify_rtp_ready = rx_st40p_rtp_ready; + /* Assembly + seq stats live in st_rx_ancillary_session; pipeline only + * routes the assembled frame. */ + ops_rx.type = ST40_TYPE_FRAME_LEVEL; + ops_rx.framebuff_cnt = ctx->framebuff_cnt; + ops_rx.framebuff_size = ops->max_udw_buff_size; + ops_rx.notify_frame_ready = rx_st40p_frame_ready; if (ops->flags & ST40P_RX_FLAG_DATA_PATH_ONLY) ops_rx.flags |= ST40_RX_FLAG_DATA_PATH_ONLY; @@ -431,13 +177,7 @@ static int rx_st40p_create_transport(struct mtl_main_impl* impl, struct st40p_rx static int rx_st40p_uinit_fbs(struct st40p_rx_ctx* ctx) { if (!ctx->framebuffs) return 0; - for (uint16_t i = 0; i < ctx->framebuff_cnt; i++) { - if (ctx->framebuffs[i].frame_info.udw_buff_addr) { - mt_rte_free(ctx->framebuffs[i].frame_info.udw_buff_addr); - ctx->framebuffs[i].frame_info.udw_buff_addr = NULL; - } - } - + /* UDW buffers are owned by the transport pool; nothing to free here. */ mt_rte_free(ctx->framebuffs); ctx->framebuffs = NULL; @@ -468,12 +208,9 @@ static int rx_st40p_init_fbs(struct st40p_rx_ctx* ctx, struct st40p_rx_ops* ops) framebuff->stat = ST40P_RX_FRAME_FREE; framebuff->idx = i; - frame_info->udw_buff_addr = mt_rte_zmalloc_socket(ops->max_udw_buff_size, soc_id); - if (!frame_info->udw_buff_addr) { - err("%s(%d), udw_buff malloc failed for frame %u\n", __func__, idx, i); - rx_st40p_uinit_fbs(ctx); - return -ENOMEM; - } + /* udw_buff_addr is bound at frame_ready time to the transport's pool + * slot; pipeline does not allocate per-frame UDW buffers. */ + frame_info->udw_buff_addr = NULL; frame_info->udw_buffer_size = ops->max_udw_buff_size; frame_info->udw_buffer_fill = 0; frame_info->meta_num = 0; @@ -649,6 +386,12 @@ int st40p_rx_put_frame(st40p_rx_handle handle, struct st40_frame_info* frame_inf goto out; } + /* release the transport-owned UDW slot back to the transport pool. */ + if (frame_info->udw_buff_addr) { + st40_rx_put_framebuff(ctx->transport, frame_info->udw_buff_addr); + frame_info->udw_buff_addr = NULL; + } + /* reset frame for reuse */ frame_info->meta_num = 0; frame_info->udw_buffer_fill = 0; @@ -692,6 +435,12 @@ int st40p_rx_put_frame_abort(st40p_rx_handle handle, struct st40_frame_info* fra goto out; } + /* release the transport-owned UDW slot back to the transport pool. */ + if (frame_info->udw_buff_addr) { + st40_rx_put_framebuff(ctx->transport, frame_info->udw_buff_addr); + frame_info->udw_buff_addr = NULL; + } + /* reset frame for reuse without processing */ frame_info->meta_num = 0; frame_info->udw_buffer_fill = 0; @@ -734,8 +483,6 @@ int st40p_rx_free(st40p_rx_handle handle) { ctx->transport = NULL; } - ctx->inflight_frame = NULL; - ctx->pending_frame = NULL; rx_st40p_uinit_fbs(ctx); mt_pthread_mutex_destroy(&ctx->lock); @@ -788,9 +535,6 @@ st40p_rx_handle st40p_rx_create(mtl_handle mt, struct st40p_rx_ops* ops) { ctx->impl = impl; ctx->type = MT_ST40_HANDLE_PIPELINE_RX; ctx->wake_on_destroy = (void (*)(void*))rx_st40p_block_wake; - ctx->session_last_seq_valid = false; - ctx->session_last_seq = 0; - for (int i = 0; i < MTL_SESSION_PORT_MAX; i++) ctx->last_seq_valid[i] = false; for (int i = 0; i < MTL_SESSION_PORT_MAX; i++) { ctx->port_map[i] = MTL_PORT_MAX; ctx->port_id[i] = UINT16_MAX; diff --git a/lib/src/st2110/pipeline/st40_pipeline_rx.h b/lib/src/st2110/pipeline/st40_pipeline_rx.h index 0de963e23..852b30ebd 100644 --- a/lib/src/st2110/pipeline/st40_pipeline_rx.h +++ b/lib/src/st2110/pipeline/st40_pipeline_rx.h @@ -14,8 +14,6 @@ extern "C" { enum st40p_rx_frame_status { ST40P_RX_FRAME_FREE = 0, - ST40P_RX_FRAME_RECEIVING, - ST40P_RX_FRAME_PENDING, ST40P_RX_FRAME_READY, ST40P_RX_FRAME_IN_USER, ST40P_RX_FRAME_STATUS_MAX, @@ -43,15 +41,6 @@ struct st40p_rx_ctx { uint16_t framebuff_producer_idx; uint16_t framebuff_consumer_idx; struct st40p_rx_frame* framebuffs; - struct st40p_rx_frame* inflight_frame; - uint32_t inflight_rtp_timestamp; - struct st40p_rx_frame* pending_frame; - uint32_t pending_rtp_timestamp; - /* session-level continuity (post-dedup) */ - bool session_last_seq_valid; - uint16_t session_last_seq; - bool last_seq_valid[MTL_SESSION_PORT_MAX]; - uint16_t last_seq[MTL_SESSION_PORT_MAX]; pthread_mutex_t lock; bool ready; diff --git a/lib/src/st2110/st_ancillary.c b/lib/src/st2110/st_ancillary.c index 6ae7eccd9..b669b852b 100644 --- a/lib/src/st2110/st_ancillary.c +++ b/lib/src/st2110/st_ancillary.c @@ -2,7 +2,12 @@ * Copyright(c) 2022 Intel Corporation */ +#include +#include + #include "../mt_log.h" +#include "../mt_platform.h" +#include "st40_api.h" typedef union anc_udw_10_6e { struct { @@ -79,11 +84,11 @@ static inline uint16_t get_parity_bits(uint16_t val) { return parity_tab[val & 0xFF]; } -static uint16_t st_ntohs(uint8_t* data) { +static uint16_t st_ntohs(const uint8_t* data) { return ((data[0] << 8) | (data[1])); } -static uint16_t get_10bit_udw(int idx, uint8_t* data) { +static uint16_t get_10bit_udw(int idx, const uint8_t* data) { int byte_offset, bit_offset; int total_bits_offset = idx * 10; /*10 bit per field */ byte_offset = total_bits_offset / 8; @@ -165,7 +170,7 @@ static void set_10bit_udw(int idx, uint16_t udw, uint8_t* data) { data[1] = (uint8_t)(val & 0xFF); } -uint16_t st40_get_udw(uint32_t idx, uint8_t* data) { +uint16_t st40_get_udw(uint32_t idx, const uint8_t* data) { return get_10bit_udw(idx, data); } @@ -173,7 +178,7 @@ void st40_set_udw(uint32_t idx, uint16_t udw, uint8_t* data) { set_10bit_udw(idx, udw, data); } -uint16_t st40_calc_checksum(uint32_t data_num, uint8_t* data) { +uint16_t st40_calc_checksum(uint32_t data_num, const uint8_t* data) { uint16_t chks = 0, udw; for (uint32_t i = 0; i < data_num; i++) { udw = get_10bit_udw(i, data); @@ -185,6 +190,105 @@ uint16_t st40_calc_checksum(uint32_t data_num, uint8_t* data) { return chks; } +uint32_t st40_rfc8331_payload_bytes(uint16_t udw_size) { + /* 10-bit words: DID, SDID, DC, UDW[udw_size], checksum. */ + uint32_t total_bits = (uint32_t)(3 + udw_size + 1) * 10; + uint32_t total_size = (total_bits + 7) / 8; + total_size = (total_size + 3) & ~0x3U; + return (uint32_t)(sizeof(struct st40_rfc8331_payload_hdr) - 4) + total_size; +} + +void st40_rfc8331_rtp_hdr_bswap(struct st40_rfc8331_rtp_hdr* hdr) { + hdr->swapped_first_hdr_chunk = htonl(hdr->swapped_first_hdr_chunk); +} + +void st40_rfc8331_payload_hdr_bswap(struct st40_rfc8331_payload_hdr* hdr) { + hdr->swapped_first_hdr_chunk = htonl(hdr->swapped_first_hdr_chunk); + hdr->swapped_second_hdr_chunk = htonl(hdr->swapped_second_hdr_chunk); +} + +int st40_rfc8331_encode_packet(uint8_t* buf, uint32_t room, const struct st40_meta* meta, + const uint8_t* udw_in, uint32_t* written) { + uint16_t udw_size = meta->udw_size; + if (udw_size > 0xFF) return -EINVAL; + uint32_t need = st40_rfc8331_payload_bytes(udw_size); + if (room < need) return -ENOSPC; + + /* Zero the body so checksum padding bits are deterministic. */ + memset(buf, 0, need); + + struct st40_rfc8331_payload_hdr* ph = (struct st40_rfc8331_payload_hdr*)buf; + ph->first_hdr_chunk.c = meta->c; + ph->first_hdr_chunk.line_number = meta->line_number; + ph->first_hdr_chunk.horizontal_offset = meta->hori_offset; + ph->first_hdr_chunk.s = meta->s; + ph->first_hdr_chunk.stream_num = meta->stream_num; + ph->second_hdr_chunk.did = st40_add_parity_bits(meta->did); + ph->second_hdr_chunk.sdid = st40_add_parity_bits(meta->sdid); + ph->second_hdr_chunk.data_count = st40_add_parity_bits(udw_size); + + st40_rfc8331_payload_hdr_bswap(ph); + + uint8_t* udw_dst = (uint8_t*)&ph->second_hdr_chunk; + for (uint16_t i = 0; i < udw_size; i++) { + st40_set_udw(i + 3, st40_add_parity_bits(udw_in[i]), udw_dst); + } + uint16_t checksum = st40_calc_checksum(3 + udw_size, udw_dst); + st40_set_udw(udw_size + 3, checksum, udw_dst); + + *written = need; + return 0; +} + +enum st40_rfc8331_decode_result st40_rfc8331_decode_packet( + const uint8_t* buf, uint32_t room, struct st40_meta* meta, uint8_t* udw_out, + uint32_t udw_cap, uint32_t* consumed) { + if (room < sizeof(struct st40_rfc8331_payload_hdr)) + return ST40_RFC8331_DECODE_SHORT_BUFFER; + + /* Read header without mutating caller memory: copy + bswap a local. */ + struct st40_rfc8331_payload_hdr hdr_local = + *(const struct st40_rfc8331_payload_hdr*)buf; + st40_rfc8331_payload_hdr_bswap(&hdr_local); + + if (!st40_check_parity_bits(hdr_local.second_hdr_chunk.did) || + !st40_check_parity_bits(hdr_local.second_hdr_chunk.sdid) || + !st40_check_parity_bits(hdr_local.second_hdr_chunk.data_count)) + return ST40_RFC8331_DECODE_PARITY_FAIL; + + uint16_t udw_size = hdr_local.second_hdr_chunk.data_count & 0xFF; + uint32_t need = st40_rfc8331_payload_bytes(udw_size); + if (room < need) return ST40_RFC8331_DECODE_SHORT_BUFFER; + if (udw_out && udw_size > udw_cap) return ST40_RFC8331_DECODE_SHORT_BUFFER; + + /* UDW/checksum reads need wire-order layout; buf is already wire-order. */ + const uint8_t* udw_src = + (const uint8_t*)&((const struct st40_rfc8331_payload_hdr*)buf)->second_hdr_chunk; + + for (uint16_t i = 0; i < udw_size; i++) { + uint16_t udw = st40_get_udw(i + 3, udw_src); + if (!st40_check_parity_bits(udw)) return ST40_RFC8331_DECODE_PARITY_FAIL; + if (udw_out) udw_out[i] = (uint8_t)(udw & 0xFF); + } + + uint16_t checksum_wire = st40_get_udw(udw_size + 3, udw_src); + uint16_t checksum_calc = st40_calc_checksum(3 + udw_size, udw_src); + if (checksum_wire != checksum_calc) return ST40_RFC8331_DECODE_CHECKSUM_FAIL; + + meta->c = hdr_local.first_hdr_chunk.c; + meta->line_number = hdr_local.first_hdr_chunk.line_number; + meta->hori_offset = hdr_local.first_hdr_chunk.horizontal_offset; + meta->s = hdr_local.first_hdr_chunk.s; + meta->stream_num = hdr_local.first_hdr_chunk.stream_num; + meta->did = hdr_local.second_hdr_chunk.did & 0xFF; + meta->sdid = hdr_local.second_hdr_chunk.sdid & 0xFF; + meta->udw_size = udw_size; + meta->udw_offset = 0; + + *consumed = need; + return ST40_RFC8331_DECODE_OK; +} + uint16_t st40_add_parity_bits(uint16_t val) { return get_parity_bits(val) | (val & 0xFF); } diff --git a/lib/src/st2110/st_header.h b/lib/src/st2110/st_header.h index a39cb3a8c..7b923d564 100644 --- a/lib/src/st2110/st_header.h +++ b/lib/src/st2110/st_header.h @@ -1115,6 +1115,42 @@ struct st_tx_ancillary_sessions_mgr { int stat_trs_ret_code[MTL_PORT_MAX]; }; +/* Frame slot state machine for ST40_TYPE_FRAME_LEVEL transport pool. */ +enum st_rx_anc_slot_state { + ST_RX_ANC_SLOT_FREE = 0, /* available for next frame */ + ST_RX_ANC_SLOT_RECEIVING, /* currently being assembled */ + ST_RX_ANC_SLOT_PENDING, /* waiting for late marker from redundant port */ + ST_RX_ANC_SLOT_READY, /* assembly complete, awaiting notify_frame_ready */ + ST_RX_ANC_SLOT_IN_USER, /* delivered to app via notify_frame_ready */ +}; + +/* Transport-owned ANC frame buffer slot (mirrors st_frame_trans / st30_frames). */ +struct st_rx_anc_frame_slot { + uint8_t* udw_buf; /* size = ops.framebuff_size */ + size_t udw_buf_size; + struct st40_meta meta[ST40_MAX_META]; + struct st40_rx_frame_meta cur_meta; /* assembled meta delivered to app */ + _Atomic enum st_rx_anc_slot_state state; + uint16_t idx; + uint32_t rtp_timestamp; /* RTP timestamp for this frame */ + uint32_t udw_buffer_fill; /* bytes written so far into udw_buf */ + uint16_t meta_num; /* populated entries in meta[] */ + uint16_t pkts_total; /* RTP packets accepted into this frame */ + uint16_t pkts_recv[MTL_SESSION_PORT_MAX]; + uint64_t receive_timestamp; /* earliest hw rx ts seen */ + bool interlaced; + bool second_field; + bool rtp_marker; + /* per-frame seq tracking (session bitmap + per-port deltas) */ + uint64_t seq_bitmap; /* 1 bit per accepted seq offset (max 64 pkts) */ + uint16_t seq_base; /* offset 0 corresponds to this seq_id */ + uint16_t seq_max_offset; /* highest offset set in bitmap (0 if empty) */ + bool seq_base_valid; + int32_t port_last_seq[MTL_SESSION_PORT_MAX]; /* -1 = none seen yet */ + uint32_t port_seq_lost[MTL_SESSION_PORT_MAX]; /* per-port intra-frame gaps */ + bool port_seq_discont[MTL_SESSION_PORT_MAX]; /* per-port discont flag */ +}; + struct st_rx_ancillary_session_impl { int idx; /* index for current session */ int socket_id; @@ -1132,6 +1168,19 @@ struct st_rx_ancillary_session_impl { struct mt_rxq_entry* rxq[MTL_SESSION_PORT_MAX]; struct rte_ring* packet_ring; + /* Frame-mode (ST40_TYPE_FRAME_LEVEL) slot pool — owned by transport. + * Allocated only when ops.type == ST40_TYPE_FRAME_LEVEL. + * In RTP_LEVEL mode these stay NULL/0 and the packet_ring path is used. */ + struct st_rx_anc_frame_slot* frame_slots; + uint16_t frame_slots_cnt; + /* inflight/pending dance for cross-port redundancy assembly. */ + struct st_rx_anc_frame_slot* anc_inflight_slot; + struct st_rx_anc_frame_slot* anc_pending_slot; + uint64_t stat_assemble_dispatched; /* count of pkts routed to FRAME_LEVEL */ + uint64_t stat_anc_frames_ready; /* frames handed to notify_frame_ready */ + uint64_t stat_anc_frames_dropped; /* frame slot pool exhausted */ + uint64_t stat_anc_pkt_parse_err; /* malformed UDW / parity / checksum */ + uint16_t st40_dst_port[MTL_SESSION_PORT_MAX]; /* udp port */ bool mcast_joined[MTL_SESSION_PORT_MAX]; int session_seq_id; /* global session seq id to track continuity across redundant */ @@ -1178,7 +1227,7 @@ struct st_rx_ancillary_session_impl { /* internal-only stats */ rte_atomic32_t stat_frames_received; /* session-total atomic for cross-thread */ uint64_t stat_last_time; - uint32_t stat_max_notify_rtp_us; + uint32_t stat_max_notify_us; /* sustained-busy hysteresis for the unified back-pressure warn line. * Single-writer (mt_stat thread). */ uint32_t stat_consecutive_busy_intervals; diff --git a/lib/src/st2110/st_rx_ancillary_session.c b/lib/src/st2110/st_rx_ancillary_session.c index 1ed7c6695..7bc59b1ed 100644 --- a/lib/src/st2110/st_rx_ancillary_session.c +++ b/lib/src/st2110/st_rx_ancillary_session.c @@ -92,6 +92,335 @@ static int rx_ancillary_sessions_tasklet_stop(void* priv) { return 0; } +/* FRAME_LEVEL assembler — ports the body of pipeline rx_st40p_rtp_ready() + * into the transport. Owns the frame slot pool; on marker, fills cur_meta + * and calls ops->notify_frame_ready(priv, addr, &meta). + * + * Slot lifecycle (single writer per side): + * FREE → tasklet picks for new frame → RECEIVING + * RECEIVING → marker arrives → READY (notify) → IN_USER + * RECEIVING → tmstamp change, num_port>1 → PENDING (wait late marker) + * PENDING → matching late marker arrives → READY (notify) → IN_USER + * IN_USER → app calls st40_rx_put_framebuff → FREE + * + * slot->state is a C11 _Atomic enum (memory_order_seq_cst): the tasklet + * thread transitions FREE→RECEIVING→...→IN_USER while the app thread + * concurrently sets IN_USER→FREE from st40_rx_put_framebuff(). + */ + +static struct st_rx_anc_frame_slot* rx_anc_get_free_slot( + struct st_rx_ancillary_session_impl* s) { + for (uint16_t i = 0; i < s->frame_slots_cnt; i++) { + struct st_rx_anc_frame_slot* slot = &s->frame_slots[i]; + if (atomic_load_explicit(&slot->state, memory_order_seq_cst) == ST_RX_ANC_SLOT_FREE) + return slot; + } + return NULL; +} + +static void rx_anc_slot_init_frame(struct st_rx_anc_frame_slot* slot, uint32_t tmstamp, + bool interlaced, bool second_field, uint64_t recv_ts) { + atomic_store_explicit(&slot->state, ST_RX_ANC_SLOT_RECEIVING, memory_order_seq_cst); + slot->rtp_timestamp = tmstamp; + slot->udw_buffer_fill = 0; + slot->meta_num = 0; + slot->pkts_total = 0; + for (int i = 0; i < MTL_SESSION_PORT_MAX; i++) slot->pkts_recv[i] = 0; + slot->receive_timestamp = recv_ts; + slot->interlaced = interlaced; + slot->second_field = second_field; + slot->rtp_marker = false; + /* seq trackers */ + slot->seq_bitmap = 0; + slot->seq_base = 0; + slot->seq_max_offset = 0; + slot->seq_base_valid = false; + for (int i = 0; i < MTL_SESSION_PORT_MAX; i++) { + slot->port_last_seq[i] = -1; + slot->port_seq_lost[i] = 0; + slot->port_seq_discont[i] = false; + } +} + +/* record one accepted packet's seq into the slot's per-frame trackers. + * - Updates the session-merged bitmap (handles backward arrivals via rebase). + * - Updates per-port last_seq + per-port lost gaps. + * Backward seqs that don't fit (frame too large for 64-bit bitmap) silently + * do not contribute to the bitmap; they still update per-port. */ +static void rx_anc_slot_record_seq(struct st_rx_anc_frame_slot* slot, uint16_t seq_id, + enum mtl_session_port s_port) { + if (!slot->seq_base_valid) { + slot->seq_base = seq_id; + slot->seq_base_valid = true; + slot->seq_bitmap = 1; /* offset 0 set */ + slot->seq_max_offset = 0; + } else { + int32_t delta = (int16_t)(seq_id - slot->seq_base); + if (delta < 0) { + /* Late backward arrival: rebase if shift fits the bitmap. */ + uint32_t shift = (uint32_t)(-delta); + if (shift < ST_RX_ANC_BITMAP_BITS) { + slot->seq_bitmap <<= shift; + slot->seq_bitmap |= 1ULL; /* new offset 0 = the late seq */ + slot->seq_base = seq_id; + slot->seq_max_offset += shift; + } + /* else: too far back to track; bitmap unchanged. */ + } else { + uint32_t off = (uint32_t)delta; + if (off < ST_RX_ANC_BITMAP_BITS) { + slot->seq_bitmap |= ((uint64_t)1 << off); + if (off > slot->seq_max_offset) slot->seq_max_offset = (uint16_t)off; + } + } + } + + if (s_port < MTL_SESSION_PORT_MAX) { + if (slot->port_last_seq[s_port] >= 0) { + uint16_t expected = (uint16_t)(slot->port_last_seq[s_port] + 1); + if (expected != seq_id) { + slot->port_seq_discont[s_port] = true; + if (mt_seq16_greater(seq_id, expected)) + slot->port_seq_lost[s_port] += (uint32_t)(uint16_t)(seq_id - expected); + } + } + /* Only advance — don't rewind on backward arrivals. */ + if (slot->port_last_seq[s_port] < 0 || + mt_seq16_greater(seq_id, (uint16_t)slot->port_last_seq[s_port])) + slot->port_last_seq[s_port] = seq_id; + } +} + +/* Parse one RTP packet's ANC chunks into the slot. Best-effort: partial + * successes are kept on validation failure (matches pipeline behavior). */ +static void rx_anc_slot_parse_pkt(struct st_rx_ancillary_session_impl* s, + struct st_rx_anc_frame_slot* slot, + struct st40_rfc8331_rtp_hdr* hdr, uint16_t len) { + uint32_t anc_count = hdr->first_hdr_chunk.anc_count; + uint8_t* payload = (uint8_t*)(hdr + 1); + uint32_t payload_room = (len > sizeof(*hdr)) ? (len - sizeof(*hdr)) : 0; + uint32_t payload_offset = 0; + + for (uint32_t anc_idx = 0; anc_idx < anc_count; anc_idx++) { + if (slot->meta_num >= ST40_MAX_META) { + warn("%s(%d), meta slots exhausted at %u\n", __func__, s->idx, slot->meta_num); + break; + } + + struct st40_meta* meta_entry = &slot->meta[slot->meta_num]; + uint32_t udw_offset = slot->udw_buffer_fill; + uint32_t consumed = 0; + enum st40_rfc8331_decode_result ret = st40_rfc8331_decode_packet( + payload + payload_offset, payload_room - payload_offset, meta_entry, + &slot->udw_buf[udw_offset], (uint32_t)(slot->udw_buf_size - udw_offset), + &consumed); + + if (ret == ST40_RFC8331_DECODE_SHORT_BUFFER) { + warn("%s(%d), anc payload exceeds available room anc=%u offset=%u room=%u\n", + __func__, s->idx, anc_idx, payload_offset, payload_room); + s->stat_anc_pkt_parse_err++; + break; + } + if (ret == ST40_RFC8331_DECODE_PARITY_FAIL) { + warn("%s(%d), UDW parity fail anc=%u\n", __func__, s->idx, anc_idx); + s->stat_anc_pkt_parse_err++; + break; + } + if (ret == ST40_RFC8331_DECODE_CHECKSUM_FAIL) { + warn("%s(%d), checksum mismatch anc=%u\n", __func__, s->idx, anc_idx); + s->stat_anc_pkt_parse_err++; + break; + } + + meta_entry->udw_offset = udw_offset; + slot->udw_buffer_fill += meta_entry->udw_size; + slot->meta_num++; + payload_offset += consumed; + } +} + +static void rx_anc_slot_deliver(struct mtl_main_impl* impl, + struct st_rx_ancillary_session_impl* s, + struct st_rx_anc_frame_slot* slot) { + struct st40_rx_ops* ops = &s->ops; + struct st40_rx_frame_meta* meta = &slot->cur_meta; + + memset(meta, 0, sizeof(*meta)); + meta->meta = slot->meta; + meta->meta_num = slot->meta_num; + meta->udw_buffer_fill = slot->udw_buffer_fill; + meta->tfmt = ST10_TIMESTAMP_FMT_MEDIA_CLK; + meta->timestamp = slot->rtp_timestamp; + meta->rtp_timestamp = slot->rtp_timestamp; + meta->interlaced = slot->interlaced; + meta->second_field = slot->second_field; + meta->rtp_marker = slot->rtp_marker; + meta->timestamp_first_pkt = slot->receive_timestamp; + /* derive seq_lost / seq_discont from this slot's own per-frame bitmap. + * expected = max_offset + 1 (slots with at least one packet). + * seen = popcount(bitmap). + * Cross-port redundancy is naturally accounted for: late R packets that + * fill in P gaps flip the corresponding bit, so they don't show as loss. */ + if (slot->seq_base_valid) { + uint32_t expected = (uint32_t)slot->seq_max_offset + 1; + uint32_t seen = (uint32_t)__builtin_popcountll(slot->seq_bitmap); + meta->seq_lost = expected > seen ? (expected - seen) : 0; + } else { + meta->seq_lost = 0; + } + meta->seq_discont = meta->seq_lost > 0; + for (int i = 0; i < MTL_SESSION_PORT_MAX; i++) { + meta->port_seq_lost[i] = slot->port_seq_lost[i]; + meta->port_seq_discont[i] = slot->port_seq_discont[i]; + meta->pkts_recv[i] = slot->pkts_recv[i]; + } + meta->pkts_total = slot->pkts_total; + meta->status = meta->seq_discont ? ST_FRAME_STATUS_CORRUPTED : ST_FRAME_STATUS_COMPLETE; + + s->stat_anc_frames_ready++; + /* stat_frames_received is bumped once per unique wire timestamp in + * handle_pkt(); do not double-count it here. */ + + /* Hand off to app. Slot ownership transfers until put_framebuff. */ + atomic_store_explicit(&slot->state, ST_RX_ANC_SLOT_IN_USER, memory_order_seq_cst); + uint64_t tsc_start = mt_sessions_time_measure(impl) ? mt_get_tsc(impl) : 0; + int ret = ops->notify_frame_ready + ? ops->notify_frame_ready(ops->priv, slot->udw_buf, meta) + : -EIO; + if (tsc_start) { + uint32_t delta_us = (mt_get_tsc(impl) - tsc_start) / NS_PER_US; + s->stat_max_notify_us = RTE_MAX(s->stat_max_notify_us, delta_us); + } + if (ret < 0) { + /* App rejected — reclaim slot ourselves so the pool isn't starved. */ + err("%s(%d), notify_frame_ready ret %d, reclaim slot %u\n", __func__, s->idx, ret, + slot->idx); + atomic_store_explicit(&slot->state, ST_RX_ANC_SLOT_FREE, memory_order_seq_cst); + } +} + +/* Force-deliver a parked PENDING slot that will not receive its late marker + * (evicted by a rollover, or stale during pool exhaustion). */ +static void rx_anc_force_deliver_pending(struct mtl_main_impl* impl, + struct st_rx_ancillary_session_impl* s) { + struct st_rx_anc_frame_slot* prev_pending = s->anc_pending_slot; + s->anc_pending_slot = NULL; + atomic_store_explicit(&prev_pending->state, ST_RX_ANC_SLOT_READY, memory_order_seq_cst); + rx_anc_slot_deliver(impl, s, prev_pending); +} + +/* Pick or allocate the target slot for tmstamp, handling the + * pending/inflight/eviction state machine. Returns NULL if the frame slot + * pool is exhausted and the packet must be dropped. */ +static struct st_rx_anc_frame_slot* rx_anc_select_slot( + struct mtl_main_impl* impl, struct st_rx_ancillary_session_impl* s, uint32_t tmstamp, + bool interlaced, bool second_field, uint64_t recv_ts) { + struct st_rx_anc_frame_slot* slot = NULL; + /* Snapshot to distinguish a pending slot that predates this call (safe to + * force-deliver on pool exhaustion) from one created by a rollover below in + * this same call (must still get its late-marker grace period). */ + struct st_rx_anc_frame_slot* pending_at_entry = s->anc_pending_slot; + + if (s->anc_pending_slot && s->anc_pending_slot->rtp_timestamp == tmstamp) { + /* Late packet for the pending frame — feed it. */ + slot = s->anc_pending_slot; + } else if (s->anc_inflight_slot) { + if (s->anc_inflight_slot->rtp_timestamp == tmstamp) { + slot = s->anc_inflight_slot; + } else { + /* Timestamp moved on. Roll inflight to PENDING (multi-port) or READY (single). */ + struct st_rx_anc_frame_slot* old = s->anc_inflight_slot; + s->anc_inflight_slot = NULL; + if (s->ops.num_port > 1) { + /* Force-deliver existing pending — no late marker arrived. */ + if (s->anc_pending_slot) rx_anc_force_deliver_pending(impl, s); + atomic_store_explicit(&old->state, ST_RX_ANC_SLOT_PENDING, memory_order_seq_cst); + s->anc_pending_slot = old; + } else { + atomic_store_explicit(&old->state, ST_RX_ANC_SLOT_READY, memory_order_seq_cst); + rx_anc_slot_deliver(impl, s, old); + } + } + } + + if (!slot) { + /* Need a fresh slot for this rtp_timestamp. */ + slot = rx_anc_get_free_slot(s); + if (!slot && s->anc_pending_slot && s->anc_pending_slot == pending_at_entry) { + /* Pool exhausted with a stale pending parked and no inflight to evict + * it (e.g. inflight cleared by a plain marker delivery) — force-deliver + * it now instead of stalling until inflight is repopulated. Only a + * pending slot that predates this call qualifies: one just rolled over + * above still deserves its late-marker grace period. */ + rx_anc_force_deliver_pending(impl, s); + slot = rx_anc_get_free_slot(s); + } + if (!slot) { + s->stat_anc_frames_dropped++; + dbg("%s(%d), frame slot pool exhausted, drop tmstamp %u\n", __func__, s->idx, + tmstamp); + return NULL; + } + rx_anc_slot_init_frame(slot, tmstamp, interlaced, second_field, recv_ts); + s->anc_inflight_slot = slot; + } else { + /* Existing slot: keep the earliest receive_timestamp seen. */ + if (recv_ts && (!slot->receive_timestamp || slot->receive_timestamp > recv_ts)) + slot->receive_timestamp = recv_ts; + /* Refresh interlace metadata if a later packet disagrees with our guess. */ + if (slot->interlaced != interlaced || slot->second_field != second_field) { + slot->interlaced = interlaced; + slot->second_field = second_field; + } + } + + return slot; +} + +static int rx_ancillary_session_assemble_pkt(struct mtl_main_impl* impl, + struct st_rx_ancillary_session_impl* s, + struct rte_mbuf* mbuf, uint16_t seq_id, + uint32_t tmstamp, + enum mtl_session_port s_port) { + s->stat_assemble_dispatched++; + + /* Re-derive header pointer (handle_pkt has already ntohl'd first chunk). */ + size_t hdr_offset = sizeof(struct st_rfc3550_hdr) - sizeof(struct st_rfc3550_rtp_hdr); + struct st40_rfc8331_rtp_hdr* hdr = + rte_pktmbuf_mtod_offset(mbuf, struct st40_rfc8331_rtp_hdr*, hdr_offset); + + uint8_t f_bits = hdr->first_hdr_chunk.f & 0x3; + bool pkt_interlaced = (f_bits & 0x2) ? true : false; + bool pkt_second_field = pkt_interlaced && (f_bits & 0x1); + bool marker = hdr->base.marker ? true : false; + uint16_t len = mbuf->data_len - hdr_offset; + enum mtl_port port = mt_port_logic2phy(s->port_maps, s_port); + uint64_t recv_ts = mt_mbuf_time_stamp(impl, mbuf, port); + + struct st_rx_anc_frame_slot* slot = + rx_anc_select_slot(impl, s, tmstamp, pkt_interlaced, pkt_second_field, recv_ts); + if (!slot) return -EBUSY; + + slot->pkts_total++; + if (s_port < MTL_SESSION_PORT_MAX) slot->pkts_recv[s_port]++; + rx_anc_slot_record_seq(slot, seq_id, s_port); + + /* Parse ANC payload into slot. Errors are recorded; assembly continues + * because partial frames are still useful (mirrors pipeline behavior). */ + rx_anc_slot_parse_pkt(s, slot, hdr, len); + + if (marker) { + slot->rtp_marker = true; + if (slot == s->anc_pending_slot) + s->anc_pending_slot = NULL; + else + s->anc_inflight_slot = NULL; + atomic_store_explicit(&slot->state, ST_RX_ANC_SLOT_READY, memory_order_seq_cst); + rx_anc_slot_deliver(impl, s, slot); + } + return 0; +} + static int rx_ancillary_session_handle_pkt(struct mtl_main_impl* impl, struct st_rx_ancillary_session_impl* s, struct rte_mbuf* mbuf, @@ -321,18 +650,24 @@ static int rx_ancillary_session_handle_pkt(struct mtl_main_impl* impl, } } - /* enqueue to packet ring to let app to handle */ - int ret = rte_ring_sp_enqueue(s->packet_ring, (void*)mbuf); - if (ret < 0) { - err("%s(%d), can not enqueue to the rte ring, packet drop, pkt seq %d\n", __func__, - s->idx, seq_id); - ST40_FUZZ_LOG("%s(%d,%d), enqueue failure for seq %u len %u\n", __func__, s->idx, - s_port, seq_id, pkt_len); - s->port_user_stats.stat_pkts_enqueue_fail++; - MT_USDT_ST40_RX_MBUF_ENQUEUE_FAIL(s->mgr->idx, s->idx, mbuf, tmstamp); - return 0; + /* enqueue to packet ring (RTP_LEVEL) or hand off to assembler (FRAME_LEVEL) */ + if (s->ops.type == ST40_TYPE_FRAME_LEVEL) { + /* dispatch only — assembler stub does not take an mbuf refcnt; the + * caller's burst-free reclaims it. RTP-style notify is skipped. */ + rx_ancillary_session_assemble_pkt(impl, s, mbuf, seq_id, tmstamp, s_port); + } else { + int ret = rte_ring_sp_enqueue(s->packet_ring, (void*)mbuf); + if (ret < 0) { + err("%s(%d), can not enqueue to the rte ring, packet drop, pkt seq %d\n", __func__, + s->idx, seq_id); + ST40_FUZZ_LOG("%s(%d,%d), enqueue failure for seq %u len %u\n", __func__, s->idx, + s_port, seq_id, pkt_len); + s->port_user_stats.stat_pkts_enqueue_fail++; + MT_USDT_ST40_RX_MBUF_ENQUEUE_FAIL(s->mgr->idx, s->idx, mbuf, tmstamp); + return 0; + } + rte_mbuf_refcnt_update(mbuf, 1); /* free when app put */ } - rte_mbuf_refcnt_update(mbuf, 1); /* free when app put */ /* Only advance timestamp forward — prev-frame late arrivals must not roll back. * Exception: threshold bypass is a recovery mechanism that may legitimately @@ -351,10 +686,12 @@ static int rx_ancillary_session_handle_pkt(struct mtl_main_impl* impl, uint64_t tsc_start = 0; bool time_measure = mt_sessions_time_measure(impl); if (time_measure) tsc_start = mt_get_tsc(impl); - ops->notify_rtp_ready(ops->priv); + if (s->ops.type != ST40_TYPE_FRAME_LEVEL) { + ops->notify_rtp_ready(ops->priv); + } if (time_measure) { uint32_t delta_us = (mt_get_tsc(impl) - tsc_start) / NS_PER_US; - s->stat_max_notify_rtp_us = RTE_MAX(s->stat_max_notify_rtp_us, delta_us); + s->stat_max_notify_us = RTE_MAX(s->stat_max_notify_us, delta_us); } MT_USDT_ST40_RX_MBUF_AVAILABLE(s->mgr->idx, s->idx, mbuf, tmstamp, pkt_len); @@ -373,8 +710,9 @@ static void rx_ancillary_session_reset(struct st_rx_ancillary_session_impl* s, memset(&s->anc_window_cur, 0, sizeof(s->anc_window_cur)); memset(&s->anc_window_prev, 0, sizeof(s->anc_window_prev)); s->stat_last_time = init_stat_time_now ? mt_get_monotonic_time() : 0; - s->stat_max_notify_rtp_us = 0; + s->stat_max_notify_us = 0; s->stat_consecutive_busy_intervals = 0; + s->stat_assemble_dispatched = 0; rte_atomic32_set(&s->stat_frames_received, 0); mt_stat_u64_init(&s->stat_time); memset(&s->port_user_stats, 0, sizeof(s->port_user_stats)); @@ -551,6 +889,51 @@ static int rx_ancillary_session_init_mcast(struct mtl_main_impl* impl, return 0; } +static int rx_ancillary_session_uinit_frames(struct st_rx_ancillary_session_impl* s) { + if (s->frame_slots) { + for (uint16_t i = 0; i < s->frame_slots_cnt; i++) { + if (s->frame_slots[i].udw_buf) { + mt_rte_free(s->frame_slots[i].udw_buf); + s->frame_slots[i].udw_buf = NULL; + } + } + mt_rte_free(s->frame_slots); + s->frame_slots = NULL; + } + s->frame_slots_cnt = 0; + return 0; +} + +static int rx_ancillary_session_init_frames(struct st_rx_ancillary_session_impl* s) { + uint16_t cnt = s->ops.framebuff_cnt; + size_t buf_sz = s->ops.framebuff_size; + int idx = s->idx; + + s->frame_slots = mt_rte_zmalloc_socket(sizeof(*s->frame_slots) * cnt, s->socket_id); + if (!s->frame_slots) { + err("%s(%d), frame_slots alloc fail (%u slots)\n", __func__, idx, cnt); + return -ENOMEM; + } + s->frame_slots_cnt = cnt; + + for (uint16_t i = 0; i < cnt; i++) { + struct st_rx_anc_frame_slot* slot = &s->frame_slots[i]; + slot->idx = i; + atomic_store_explicit(&slot->state, ST_RX_ANC_SLOT_FREE, memory_order_seq_cst); + slot->udw_buf_size = buf_sz; + slot->udw_buf = mt_rte_zmalloc_socket(buf_sz, s->socket_id); + if (!slot->udw_buf) { + err("%s(%d), udw_buf alloc fail (%zu bytes) for slot %u\n", __func__, idx, buf_sz, + i); + rx_ancillary_session_uinit_frames(s); + return -ENOMEM; + } + } + + info("%s(%d), %u frame slots, %zu bytes UDW each\n", __func__, idx, cnt, buf_sz); + return 0; +} + static int rx_ancillary_session_init_sw(struct st_rx_ancillary_sessions_mgr* mgr, struct st_rx_ancillary_session_impl* s) { char ring_name[32]; @@ -558,6 +941,10 @@ static int rx_ancillary_session_init_sw(struct st_rx_ancillary_sessions_mgr* mgr unsigned int flags, count; int mgr_idx = mgr->idx, idx = s->idx; + if (s->ops.type == ST40_TYPE_FRAME_LEVEL) { + return rx_ancillary_session_init_frames(s); + } + snprintf(ring_name, 32, "%sM%dS%d_PKT", ST_RX_ANCILLARY_PREFIX, mgr_idx, idx); flags = RING_F_SP_ENQ | RING_F_SC_DEQ; /* single-producer and single-consumer */ count = s->ops.rtp_ring_size; @@ -581,6 +968,7 @@ static int rx_ancillary_session_uinit_sw(struct st_rx_ancillary_session_impl* s) rte_ring_free(s->packet_ring); s->packet_ring = NULL; } + rx_ancillary_session_uinit_frames(s); return 0; } @@ -785,6 +1173,19 @@ static void rx_ancillary_session_stat(struct st_rx_ancillary_session_impl* s) { idx, interlace_first_field, interlace_second_field); } + /* assembler observability for FRAME_LEVEL transport */ + if (s->stat_anc_frames_dropped || s->stat_anc_pkt_parse_err || + s->stat_assemble_dispatched) { + notice("RX_ANC_SESSION(%d): assembler dispatched %" PRIu64 " ready %" PRIu64 + " dropped %" PRIu64 " parse_err %" PRIu64 "\n", + idx, s->stat_assemble_dispatched, s->stat_anc_frames_ready, + s->stat_anc_frames_dropped, s->stat_anc_pkt_parse_err); + s->stat_assemble_dispatched = 0; + s->stat_anc_frames_ready = 0; + s->stat_anc_frames_dropped = 0; + s->stat_anc_pkt_parse_err = 0; + } + memcpy(snap, us, sizeof(*snap)); struct mt_stat_u64* stat_time = &s->stat_time; @@ -795,10 +1196,10 @@ static void rx_ancillary_session_stat(struct st_rx_ancillary_session_impl* s) { (float)stat_time->min / NS_PER_US); mt_stat_u64_init(stat_time); } - if (s->stat_max_notify_rtp_us > 8) { - notice("RX_ANC_SESSION(%d): notify rtp max %uus\n", idx, s->stat_max_notify_rtp_us); + if (s->stat_max_notify_us > 8) { + notice("RX_ANC_SESSION(%d): notify max %uus\n", idx, s->stat_max_notify_us); } - s->stat_max_notify_rtp_us = 0; + s->stat_max_notify_us = 0; } static int rx_ancillary_session_detach(struct mtl_main_impl* impl, @@ -893,6 +1294,12 @@ static int rx_ancillary_sessions_mgr_init(struct mtl_main_impl* impl, int idx = sch->idx; struct mtl_tasklet_ops ops; + /* rx_anc_slot_parse_pkt sums up to ST40_MAX_META entries of at most 0xFF + * (wire 8-bit data_count) bytes each into udw_buffer_fill, then narrows + * that running sum into st40_meta.udw_offset (uint16_t). Catch it at + * compile time if ST40_MAX_META ever grows enough to overflow that field. */ + RTE_BUILD_BUG_ON(ST40_MAX_META * 0xFF > UINT16_MAX); + mgr->parent = impl; mgr->idx = idx; @@ -1091,14 +1498,29 @@ static int rx_ancillary_ops_check(struct st40_rx_ops* ops) { } } - if (ops->rtp_ring_size <= 0) { - err("%s, invalid rtp_ring_size %d\n", __func__, ops->rtp_ring_size); - return -EINVAL; - } - - if (!ops->notify_rtp_ready) { - err("%s, pls set notify_rtp_ready\n", __func__); - return -EINVAL; + if (ops->type == ST40_TYPE_FRAME_LEVEL) { + if (!ops->notify_frame_ready) { + err("%s, FRAME_LEVEL: pls set notify_frame_ready\n", __func__); + return -EINVAL; + } + if (ops->framebuff_cnt < 2) { + err("%s, FRAME_LEVEL: framebuff_cnt %u must be >= 2\n", __func__, + ops->framebuff_cnt); + return -EINVAL; + } + if (ops->framebuff_size == 0) { + err("%s, FRAME_LEVEL: framebuff_size must be > 0\n", __func__); + return -EINVAL; + } + } else { /* ST40_TYPE_RTP_LEVEL */ + if (ops->rtp_ring_size <= 0) { + err("%s, invalid rtp_ring_size %d\n", __func__, ops->rtp_ring_size); + return -EINVAL; + } + if (!ops->notify_rtp_ready) { + err("%s, pls set notify_rtp_ready\n", __func__); + return -EINVAL; + } } /* Zero means disable the payload_type check */ @@ -1158,7 +1580,7 @@ st40_rx_handle st40_rx_create(mtl_handle mt, struct st40_rx_ops* ops) { ret = rx_ancillary_ops_check(ops); if (ret < 0) { - err("%s, st_rx_audio_ops_check fail %d\n", __func__, ret); + err("%s, rx_ancillary_ops_check fail %d\n", __func__, ret); return NULL; } @@ -1332,6 +1754,36 @@ void st40_rx_put_mbuf(st40_rx_handle handle, void* mbuf) { MT_HANDLE_RELEASE(s_impl); } +int st40_rx_put_framebuff(st40_rx_handle handle, void* frame) { + struct st_rx_ancillary_session_handle_impl* s_impl = handle; + struct st_rx_ancillary_session_impl* s; + int ret; + + MT_HANDLE_GUARD(s_impl, MT_HANDLE_RX_ANC, -EIO); + + s = s_impl->impl; + if (!s->frame_slots) { + err("%s(%d), session is not in FRAME_LEVEL mode\n", __func__, s->idx); + ret = -EIO; + goto out; + } + + for (uint16_t i = 0; i < s->frame_slots_cnt; i++) { + struct st_rx_anc_frame_slot* slot = &s->frame_slots[i]; + if (slot->udw_buf == frame) { + atomic_store_explicit(&slot->state, ST_RX_ANC_SLOT_FREE, memory_order_seq_cst); + ret = 0; + goto out; + } + } + + err("%s(%d), invalid frame %p\n", __func__, s->idx, frame); + ret = -EIO; +out: + MT_HANDLE_RELEASE(s_impl); + return ret; +} + int st40_rx_get_queue_meta(st40_rx_handle handle, struct st_queue_meta* meta) { struct st_rx_ancillary_session_handle_impl* s_impl = handle; struct st_rx_ancillary_session_impl* s; diff --git a/lib/src/st2110/st_rx_video_session.c b/lib/src/st2110/st_rx_video_session.c index 6bad1d237..b5a0c1875 100644 --- a/lib/src/st2110/st_rx_video_session.c +++ b/lib/src/st2110/st_rx_video_session.c @@ -1158,8 +1158,8 @@ static void rv_slice_add(struct st_rx_video_session_impl* s, } static struct st_rx_video_slot_impl* rv_slot_by_tmstamp( - struct st_rx_video_session_impl* s, uint32_t tmstamp, void* hdr_split_pd, - bool* exist_ts) { + struct st_rx_video_session_impl* s, struct rte_mbuf* mbuf, + enum mtl_session_port s_port, uint32_t tmstamp, void* hdr_split_pd, bool* exist_ts) { int i, slot_idx; struct st_rx_video_slot_impl* slot; @@ -1289,7 +1289,8 @@ static struct st_rx_video_slot_impl* rv_slot_by_tmstamp( } frame_info->user_meta_data_size = 0; slot->frame = frame_info; - slot->timestamp_first_pkt = mtl_ptp_read_time(rv_get_impl(s)); + slot->timestamp_first_pkt = + mt_mbuf_time_stamp(rv_get_impl(s), mbuf, mt_port_logic2phy(s->port_maps, s_port)); s->dma_slot = slot; @@ -1628,7 +1629,8 @@ static int rv_handle_frame_pkt(struct st_rx_video_session_impl* s, struct rte_mb /* find the target slot by tmstamp */ bool exist_ts = false; - struct st_rx_video_slot_impl* slot = rv_slot_by_tmstamp(s, tmstamp, NULL, &exist_ts); + struct st_rx_video_slot_impl* slot = + rv_slot_by_tmstamp(s, mbuf, s_port, tmstamp, NULL, &exist_ts); /* Based on rv_slot_by_tmstamp - exist_ts is only true when slot is found */ if (exist_ts && !slot->frame) { s->port_user_stats.common.stat_pkts_redundant++; @@ -2060,7 +2062,8 @@ static int rv_handle_st22_pkt(struct st_rx_video_session_impl* s, struct rte_mbu /* find the target slot by tmstamp */ bool exist_ts = false; - struct st_rx_video_slot_impl* slot = rv_slot_by_tmstamp(s, tmstamp, NULL, &exist_ts); + struct st_rx_video_slot_impl* slot = + rv_slot_by_tmstamp(s, mbuf, s_port, tmstamp, NULL, &exist_ts); /* Based on rv_slot_by_tmstamp - exist_ts is only true when slot is found */ if (exist_ts && !slot->frame) { s->port_user_stats.common.stat_pkts_redundant++; @@ -2237,7 +2240,8 @@ static int rv_handle_hdr_split_pkt(struct st_rx_video_session_impl* s, /* find the target slot by tmstamp */ bool exist_ts = false; - struct st_rx_video_slot_impl* slot = rv_slot_by_tmstamp(s, tmstamp, payload, &exist_ts); + struct st_rx_video_slot_impl* slot = + rv_slot_by_tmstamp(s, mbuf, s_port, tmstamp, payload, &exist_ts); /* Based on rv_slot_by_tmstamp - exist_ts is only true when slot is found */ if (exist_ts && !slot->frame) { s->port_user_stats.common.stat_pkts_redundant++; diff --git a/lib/src/st2110/st_tx_ancillary_session.c b/lib/src/st2110/st_tx_ancillary_session.c index 43a36fa39..c5322e79c 100644 --- a/lib/src/st2110/st_tx_ancillary_session.c +++ b/lib/src/st2110/st_tx_ancillary_session.c @@ -85,12 +85,6 @@ static inline void tx_ancillary_set_rtp_seq(struct st_tx_ancillary_session_impl* tx_ancillary_seq_advance(s, step); } -static inline uint16_t tx_ancillary_apply_parity( - struct st_tx_ancillary_session_impl* s __rte_unused, uint16_t value) { - TX_ANC_TEST_APPLY_PARITY(s, value); - return st40_add_parity_bits(value); -} - /* Abort the current frame and allow the app to recycle its buffer */ static void tx_ancillary_session_abort_frame(struct mtl_main_impl* impl, struct st_tx_ancillary_session_impl* s) { @@ -520,49 +514,36 @@ static int tx_ancillary_session_build_packet(struct st_tx_ancillary_session_impl int anc_idx = s->st40_anc_idx; int anc_count = src->meta_num; TX_ANC_TEST_CLAMP_ANC_IDX(s, anc_idx, anc_count); - int total_udw = 0; int idx = 0; for (idx = anc_idx; idx < anc_count; idx++) { uint16_t udw_size = src->meta[idx].udw_size; - total_udw += udw_size; - if (!s->split_payload && (total_udw * 10 / 8) > s->max_pkt_len) break; - struct st40_rfc8331_payload_hdr* pktBuff = - (struct st40_rfc8331_payload_hdr*)(payload); - pktBuff->first_hdr_chunk.c = src->meta[idx].c; - pktBuff->first_hdr_chunk.line_number = src->meta[idx].line_number; - pktBuff->first_hdr_chunk.horizontal_offset = src->meta[idx].hori_offset; - pktBuff->first_hdr_chunk.s = src->meta[idx].s; - pktBuff->first_hdr_chunk.stream_num = src->meta[idx].stream_num; - pktBuff->second_hdr_chunk.did = tx_ancillary_apply_parity(s, src->meta[idx].did); - pktBuff->second_hdr_chunk.sdid = tx_ancillary_apply_parity(s, src->meta[idx].sdid); - pktBuff->second_hdr_chunk.data_count = tx_ancillary_apply_parity(s, udw_size); - - pktBuff->swapped_first_hdr_chunk = htonl(pktBuff->swapped_first_hdr_chunk); - pktBuff->swapped_second_hdr_chunk = htonl(pktBuff->swapped_second_hdr_chunk); - int i = 0; - int offset = src->meta[idx].udw_offset; - for (; i < udw_size; i++) { - st40_set_udw(i + 3, tx_ancillary_apply_parity(s, src->data[offset++]), - (uint8_t*)&pktBuff->second_hdr_chunk); + uint32_t used = (uint32_t)(payload - (uint8_t*)&rtp[1]); + if (!s->split_payload && + (used + st40_rfc8331_payload_bytes(udw_size)) > s->max_pkt_len) + break; + + if (used >= s->max_pkt_len) { + err("%s(%d), ANC packet too large for MTU (used=%u max=%u)\n", __func__, s->idx, + used, s->max_pkt_len); + s->stat_build_ret_code = -STI_FRAME_ANC_TOO_LARGE; + return -STI_FRAME_ANC_TOO_LARGE; } - uint16_t checksum = 0; - checksum = st40_calc_checksum(3 + udw_size, (uint8_t*)&pktBuff->second_hdr_chunk); - st40_set_udw(i + 3, checksum, (uint8_t*)&pktBuff->second_hdr_chunk); - - /* Compute byte size of 10-bit words - (DID, SDID, DC, payload, checksum) and align to 4 */ - uint32_t total_bits = (uint32_t)(3 + udw_size + 1) * 10; /* words = udw_size + 4 */ - uint32_t total_size = (total_bits + 7) / 8; /* ceil(bits/8) */ - total_size = (total_size + 3) & ~0x3U; /* align to 4 bytes */ - uint32_t size_to_send = (sizeof(struct st40_rfc8331_payload_hdr) - 4) + - total_size; /* Full size of one ANC */ - if (s->split_payload && size_to_send > s->max_pkt_len) { + uint32_t room = s->max_pkt_len - used; + uint32_t written = 0; + int ret = st40_rfc8331_encode_packet(payload, room, &src->meta[idx], + &src->data[src->meta[idx].udw_offset], &written); + if (ret == -ENOSPC) { err("%s(%d), ANC packet too large for MTU (size=%u max=%u)\n", __func__, s->idx, - size_to_send, s->max_pkt_len); + st40_rfc8331_payload_bytes(udw_size), s->max_pkt_len); s->stat_build_ret_code = -STI_FRAME_ANC_TOO_LARGE; return -STI_FRAME_ANC_TOO_LARGE; + } else if (ret < 0) { + err("%s(%d), st40_rfc8331_encode_packet failed %d\n", __func__, s->idx, ret); + s->stat_build_ret_code = ret; + return ret; } - payload = payload + size_to_send; + TX_ANC_TEST_CORRUPT_PARITY(s, payload, udw_size); + payload += written; if (s->split_payload) { idx++; @@ -585,7 +566,7 @@ static int tx_ancillary_session_build_packet(struct st_tx_ancillary_session_impl bool last_pkt = (s->st40_total_pkts > 0) ? (s->st40_pkt_idx == (s->st40_total_pkts - 1)) : (idx == anc_count); if (!test_no_marker && last_pkt) rtp->base.marker = 1; - rtp->swapped_first_hdr_chunk = htonl(rtp->swapped_first_hdr_chunk); + st40_rfc8331_rtp_hdr_bswap(rtp); dbg("%s(%d), anc_count %d, payload_size %d\n", __func__, s->idx, anc_count, payload_size); @@ -619,49 +600,36 @@ static int tx_ancillary_session_build_rtp_packet(struct st_tx_ancillary_session_ struct st40_frame* src = frame_info->addr; int anc_count = src->meta_num; TX_ANC_TEST_CLAMP_ANC_IDX(s, anc_idx, anc_count); - int total_udw = 0; int idx = 0; for (idx = anc_idx; idx < anc_count; idx++) { uint16_t udw_size = src->meta[idx].udw_size; - total_udw += udw_size; - if (!s->split_payload && (total_udw * 10 / 8) > s->max_pkt_len) break; - struct st40_rfc8331_payload_hdr* pktBuff = - (struct st40_rfc8331_payload_hdr*)(payload); - pktBuff->first_hdr_chunk.c = src->meta[idx].c; - pktBuff->first_hdr_chunk.line_number = src->meta[idx].line_number; - pktBuff->first_hdr_chunk.horizontal_offset = src->meta[idx].hori_offset; - pktBuff->first_hdr_chunk.s = src->meta[idx].s; - pktBuff->first_hdr_chunk.stream_num = src->meta[idx].stream_num; - pktBuff->second_hdr_chunk.did = tx_ancillary_apply_parity(s, src->meta[idx].did); - pktBuff->second_hdr_chunk.sdid = tx_ancillary_apply_parity(s, src->meta[idx].sdid); - pktBuff->second_hdr_chunk.data_count = tx_ancillary_apply_parity(s, udw_size); - - pktBuff->swapped_first_hdr_chunk = htonl(pktBuff->swapped_first_hdr_chunk); - pktBuff->swapped_second_hdr_chunk = htonl(pktBuff->swapped_second_hdr_chunk); - int i = 0; - int offset = src->meta[idx].udw_offset; - for (; i < udw_size; i++) { - st40_set_udw(i + 3, tx_ancillary_apply_parity(s, src->data[offset++]), - (uint8_t*)&pktBuff->second_hdr_chunk); + uint32_t used = (uint32_t)(payload - (uint8_t*)&rtp[1]); + if (!s->split_payload && + (used + st40_rfc8331_payload_bytes(udw_size)) > s->max_pkt_len) + break; + + if (used >= s->max_pkt_len) { + err("%s(%d), ANC packet too large for MTU (used=%u max=%u)\n", __func__, s->idx, + used, s->max_pkt_len); + s->stat_build_ret_code = -STI_FRAME_ANC_TOO_LARGE; + return -STI_FRAME_ANC_TOO_LARGE; } - uint16_t checksum = 0; - checksum = st40_calc_checksum(3 + udw_size, (uint8_t*)&pktBuff->second_hdr_chunk); - st40_set_udw(i + 3, checksum, (uint8_t*)&pktBuff->second_hdr_chunk); - - /* Compute byte size of 10-bit words (DID, SDID, DC, payload, checksum) - and align to 4 */ - uint32_t total_bits = (uint32_t)(3 + udw_size + 1) * 10; - uint32_t total_size = (total_bits + 7) / 8; - total_size = (total_size + 3) & ~0x3U; - uint32_t size_to_send = (sizeof(struct st40_rfc8331_payload_hdr) - 4) + - total_size; /* Full size of one ANC */ - if (s->split_payload && size_to_send > s->max_pkt_len) { + uint32_t room = s->max_pkt_len - used; + uint32_t written = 0; + int ret = st40_rfc8331_encode_packet(payload, room, &src->meta[idx], + &src->data[src->meta[idx].udw_offset], &written); + if (ret == -ENOSPC) { err("%s(%d), ANC packet too large for MTU (size=%u max=%u)\n", __func__, s->idx, - size_to_send, s->max_pkt_len); + st40_rfc8331_payload_bytes(udw_size), s->max_pkt_len); s->stat_build_ret_code = -STI_FRAME_ANC_TOO_LARGE; return -STI_FRAME_ANC_TOO_LARGE; + } else if (ret < 0) { + err("%s(%d), st40_rfc8331_encode_packet failed %d\n", __func__, s->idx, ret); + s->stat_build_ret_code = ret; + return ret; } - payload = payload + size_to_send; + TX_ANC_TEST_CORRUPT_PARITY(s, payload, udw_size); + payload += written; if (s->split_payload) { idx++; @@ -684,7 +652,7 @@ static int tx_ancillary_session_build_rtp_packet(struct st_tx_ancillary_session_ bool last_pkt = (s->st40_total_pkts > 0) ? (s->st40_pkt_idx == (s->st40_total_pkts - 1)) : (idx == anc_count); if (!test_no_marker && last_pkt) rtp->base.marker = 1; - rtp->swapped_first_hdr_chunk = htonl(rtp->swapped_first_hdr_chunk); + st40_rfc8331_rtp_hdr_bswap(rtp); dbg("%s(%d), anc_count %d, payload_size %d\n", __func__, s->idx, anc_count, payload_size); @@ -721,7 +689,7 @@ static int tx_ancillary_session_rtp_update_packet(struct mtl_main_impl* impl, if (s->ops.interlaced) { struct st40_rfc8331_rtp_hdr* rfc8331 = (struct st40_rfc8331_rtp_hdr*)rtp; second_field = (rfc8331->first_hdr_chunk.f == 0b11) ? true : false; - rfc8331->swapped_first_hdr_chunk = htonl(rfc8331->swapped_first_hdr_chunk); + st40_rfc8331_rtp_hdr_bswap(rfc8331); } if (s->ops.interlaced) { if (second_field) { @@ -787,7 +755,7 @@ static int tx_ancillary_session_build_packet_chain(struct mtl_main_impl* impl, if (s->ops.interlaced) { struct st40_rfc8331_rtp_hdr* rfc8331 = (struct st40_rfc8331_rtp_hdr*)&udp[1]; second_field = (rfc8331->first_hdr_chunk.f == 0b11) ? true : false; - rfc8331->swapped_first_hdr_chunk = htonl(rfc8331->swapped_first_hdr_chunk); + st40_rfc8331_rtp_hdr_bswap(rfc8331); } if (s->ops.interlaced) { if (second_field) { @@ -801,7 +769,7 @@ static int tx_ancillary_session_build_packet_chain(struct mtl_main_impl* impl, ntohl(rtp->base.tmstamp)); } rtp->base.tmstamp = htonl(s->pacing.rtp_time_stamp); - rtp->swapped_first_hdr_chunk = htonl(rtp->swapped_first_hdr_chunk); + st40_rfc8331_rtp_hdr_bswap(rtp); } } diff --git a/lib/src/st2110/st_tx_ancillary_test.h b/lib/src/st2110/st_tx_ancillary_test.h index d4e888305..40a0db4f3 100644 --- a/lib/src/st2110/st_tx_ancillary_test.h +++ b/lib/src/st2110/st_tx_ancillary_test.h @@ -16,11 +16,29 @@ #ifndef _ST_TX_ANCILLARY_TEST_H #define _ST_TX_ANCILLARY_TEST_H +#include "../mt_log.h" #include "st_header.h" /* Number of entries in the redundant-path seq-gap schedule table. */ #define ST40_SEQ_GAP_SCHEDULE_LEN 8 +/** + * Corrupt the parity bits of an already-encoded RFC 8331 sub-packet in place, + * recomputing the checksum so only parity (not checksum) validation fails on + * RX. Operates on the wire buffer alone; unconditional so it can be unit + * tested without MTL_SIMULATE_PACKET_DROPS. + * @p buf points at the sub-packet's st40_rfc8331_payload_hdr, @p udw_size is + * the packet's UDW count. + */ +static inline void tx_ancillary_corrupt_parity(uint8_t* buf, uint16_t udw_size) { + uint8_t* udw_dst = (uint8_t*)&((struct st40_rfc8331_payload_hdr*)buf)->second_hdr_chunk; + /* Top 2 of each 10-bit word are parity (st40_add_parity_bits()); & 0xFF strips them. */ + for (uint16_t i = 0; i < (uint16_t)(3 + udw_size); i++) + st40_set_udw(i, st40_get_udw(i, udw_dst) & 0xFF, udw_dst); + uint16_t cksum = st40_calc_checksum(3 + udw_size, udw_dst); + st40_set_udw(3 + udw_size, cksum, udw_dst); +} + /* ------------------------------------------------------------------ */ #ifdef MTL_SIMULATE_PACKET_DROPS /* ======================== DEBUG BUILD =========================== */ @@ -49,16 +67,14 @@ static inline bool tx_ancillary_test_frame_active( } while (0) /** - * Return parity-corrupted value when the BAD_PARITY pattern is active, - * otherwise fall through to st40_add_parity_bits(). + * Apply tx_ancillary_corrupt_parity() to @p _buf when the BAD_PARITY pattern + * is active. No-op otherwise. */ -#define TX_ANC_TEST_APPLY_PARITY(_s, _val) \ - do { \ - if (tx_ancillary_test_frame_active(_s) && \ - (_s)->test.pattern == ST40_TX_TEST_BAD_PARITY) { \ - uint16_t _stripped = (_val); \ - return _stripped & 0x3FF; /* strip parity bits to corrupt */ \ - } \ +#define TX_ANC_TEST_CORRUPT_PARITY(_s, _buf, _udw_size) \ + do { \ + if (tx_ancillary_test_frame_active(_s) && \ + (_s)->test.pattern == ST40_TX_TEST_BAD_PARITY) \ + tx_ancillary_corrupt_parity((uint8_t*)(_buf), (_udw_size)); \ } while (0) /** @@ -216,8 +232,8 @@ static inline bool tx_ancillary_test_frame_active( #define TX_ANC_TEST_SEQ_STEP(_s, _step) \ do { \ } while (0) -#define TX_ANC_TEST_APPLY_PARITY(_s, _val) \ - do { \ +#define TX_ANC_TEST_CORRUPT_PARITY(_s, _buf, _udw_size) \ + do { \ } while (0) #define TX_ANC_TEST_SEQ_GAP_PLAN(_s) \ do { \ diff --git a/tests/integration_tests/noctx/testcases/st40i_tests.cpp b/tests/integration_tests/noctx/testcases/st40i_tests.cpp index 101c4ef22..c58a336f9 100644 --- a/tests/integration_tests/noctx/testcases/st40i_tests.cpp +++ b/tests/integration_tests/noctx/testcases/st40i_tests.cpp @@ -119,15 +119,12 @@ static void build_split_rtp_packet(std::vector& out, uint16_t seq, uint uint16_t checksum = st40_calc_checksum(3 + payload.size(), udw_dst); st40_set_udw(static_cast(payload.size() + 3), checksum, udw_dst); - uint32_t total_bits = (3 + payload.size() + 1) * 10; - uint32_t total_size = (total_bits + 7) / 8; - uint32_t total_size_aligned = (total_size + 3) & ~0x3u; - uint32_t payload_bytes = sizeof(st40_rfc8331_payload_hdr) - 4 + total_size_aligned; + uint32_t payload_bytes = + st40_rfc8331_payload_bytes(static_cast(payload.size())); rtp->length = htons(payload_bytes); - rtp->swapped_first_hdr_chunk = htonl(rtp->swapped_first_hdr_chunk); - ph->swapped_first_hdr_chunk = htonl(ph->swapped_first_hdr_chunk); - ph->swapped_second_hdr_chunk = htonl(ph->swapped_second_hdr_chunk); + st40_rfc8331_rtp_hdr_bswap(rtp); + st40_rfc8331_payload_hdr_bswap(ph); out.resize(sizeof(st40_rfc8331_rtp_hdr) + payload_bytes); } diff --git a/tests/integration_tests/st40_test.cpp b/tests/integration_tests/st40_test.cpp index 3a5e05c94..3ff67f406 100644 --- a/tests/integration_tests/st40_test.cpp +++ b/tests/integration_tests/st40_test.cpp @@ -53,7 +53,7 @@ static int tx_anc_build_rtp_packet(tests_context* s, struct st40_rfc8331_rtp_hdr if (s->check_sha) { struct st40_rfc8331_payload_hdr* payload_hdr = (struct st40_rfc8331_payload_hdr*)(&rtp[1]); - int total_size, payload_len, udw_size = s->frame_size; + int udw_size = s->frame_size; payload_hdr->first_hdr_chunk.c = 0; payload_hdr->first_hdr_chunk.line_number = 10; payload_hdr->first_hdr_chunk.horizontal_offset = 0; @@ -62,8 +62,7 @@ static int tx_anc_build_rtp_packet(tests_context* s, struct st40_rfc8331_rtp_hdr payload_hdr->second_hdr_chunk.did = st40_add_parity_bits(0x43); payload_hdr->second_hdr_chunk.sdid = st40_add_parity_bits(0x02); payload_hdr->second_hdr_chunk.data_count = st40_add_parity_bits(udw_size); - payload_hdr->swapped_first_hdr_chunk = htonl(payload_hdr->swapped_first_hdr_chunk); - payload_hdr->swapped_second_hdr_chunk = htonl(payload_hdr->swapped_second_hdr_chunk); + st40_rfc8331_payload_hdr_bswap(payload_hdr); rtp->first_hdr_chunk.anc_count = 1; for (int i = 0; i < udw_size; i++) { st40_set_udw(i + 3, @@ -73,13 +72,7 @@ static int tx_anc_build_rtp_packet(tests_context* s, struct st40_rfc8331_rtp_hdr uint16_t check_sum = st40_calc_checksum(3 + udw_size, (uint8_t*)&payload_hdr->second_hdr_chunk); st40_set_udw(udw_size + 3, check_sum, (uint8_t*)&payload_hdr->second_hdr_chunk); - total_size = ((3 + udw_size + 1) * 10) / 8; // Calculate size of the - // 10-bit words: DID, SDID, DATA_COUNT - // + size of buffer with data + checksum - total_size = (4 - total_size % 4) + total_size; // Calculate word align to the 32-bit - // word of ANC data packet - payload_len = - sizeof(struct st40_rfc8331_payload_hdr) - 4 + total_size; // Full size of one ANC + uint32_t payload_len = st40_rfc8331_payload_bytes(udw_size); rtp->length = htons(payload_len); *pkt_len = payload_len + sizeof(struct st40_rfc8331_rtp_hdr); } else { @@ -132,11 +125,10 @@ static void rx_handle_rtp(tests_context* s, struct st40_rfc8331_rtp_hdr* hdr) { struct st40_rfc8331_payload_hdr* payload_hdr = (struct st40_rfc8331_payload_hdr*)(&hdr[1]); int anc_count = hdr->first_hdr_chunk.anc_count; - int idx, total_size, payload_len; + int idx; for (idx = 0; idx < anc_count; idx++) { - payload_hdr->swapped_first_hdr_chunk = ntohl(payload_hdr->swapped_first_hdr_chunk); - payload_hdr->swapped_second_hdr_chunk = ntohl(payload_hdr->swapped_second_hdr_chunk); + st40_rfc8331_payload_hdr_bswap(payload_hdr); if (!st40_check_parity_bits(payload_hdr->second_hdr_chunk.did) || !st40_check_parity_bits(payload_hdr->second_hdr_chunk.sdid) || !st40_check_parity_bits(payload_hdr->second_hdr_chunk.data_count)) { @@ -173,14 +165,9 @@ static void rx_handle_rtp(tests_context* s, struct st40_rfc8331_rtp_hdr* hdr) { s->cv.notify_all(); } - total_size = ((3 + udw_size + 1) * 10) / 8; // Calculate size of the - // 10-bit words: DID, SDID, DATA_COUNT - // + size of buffer with data + checksum - total_size = (4 - total_size % 4) + total_size; // Calculate word align to the 32-bit - // word of ANC data packet - payload_len = - sizeof(struct st40_rfc8331_payload_hdr) - 4 + total_size; // Full size of one ANC - payload_hdr = (struct st40_rfc8331_payload_hdr*)((uint8_t*)payload_hdr + payload_len); + payload_hdr = + (struct st40_rfc8331_payload_hdr*)((uint8_t*)payload_hdr + + st40_rfc8331_payload_bytes(udw_size)); } } @@ -227,6 +214,7 @@ static void st40_rx_ops_init(tests_context* st40, struct st40_rx_ops* ops) { ctx->para.port[MTL_PORT_R]); ops->udp_port[MTL_SESSION_PORT_R] = 30000 + st40->idx * 2; } + ops->type = ST40_TYPE_RTP_LEVEL; ops->notify_rtp_ready = rx_rtp_ready; ops->rtp_ring_size = 1024; ops->payload_type = ST40_TEST_PAYLOAD_TYPE; @@ -589,6 +577,7 @@ static void st40_rx_fps_test(enum st40_type type[], enum st_fps fps[], snprintf(ops_rx.port[MTL_SESSION_PORT_P], MTL_PORT_MAX_LEN, "%s", ctx->para.port[MTL_PORT_R]); ops_rx.udp_port[MTL_SESSION_PORT_P] = 30000 + i * 2; + ops_rx.type = ST40_TYPE_RTP_LEVEL; ops_rx.notify_rtp_ready = rx_rtp_ready; ops_rx.rtp_ring_size = 1024; ops_rx.payload_type = ST40_TEST_PAYLOAD_TYPE; @@ -865,6 +854,7 @@ static void st40_rx_update_src_test(enum st40_type type, int tx_sessions, snprintf(ops_rx.port[MTL_SESSION_PORT_P], MTL_PORT_MAX_LEN, "%s", ctx->para.port[MTL_PORT_R]); ops_rx.udp_port[MTL_SESSION_PORT_P] = 30000 + i * 2; + ops_rx.type = ST40_TYPE_RTP_LEVEL; ops_rx.notify_rtp_ready = rx_rtp_ready; ops_rx.rtp_ring_size = 1024; ops_rx.payload_type = ST40_TEST_PAYLOAD_TYPE; @@ -1112,6 +1102,7 @@ static void st40_after_start_test(enum st40_type type[], enum st_fps fps[], int snprintf(ops_rx.port[MTL_SESSION_PORT_P], MTL_PORT_MAX_LEN, "%s", ctx->para.port[MTL_PORT_R]); ops_rx.udp_port[MTL_SESSION_PORT_P] = 30000 + i * 2; + ops_rx.type = ST40_TYPE_RTP_LEVEL; ops_rx.notify_rtp_ready = rx_rtp_ready; ops_rx.rtp_ring_size = 1024; ops_rx.payload_type = ST40_TEST_PAYLOAD_TYPE; diff --git a/tests/tools/RxTxApp/src/rx_ancillary_app.c b/tests/tools/RxTxApp/src/rx_ancillary_app.c index bc54bb230..149d1080d 100644 --- a/tests/tools/RxTxApp/src/rx_ancillary_app.c +++ b/tests/tools/RxTxApp/src/rx_ancillary_app.c @@ -10,12 +10,11 @@ static void app_rx_anc_handle_rtp(struct st_app_rx_anc_session* s, void* usrptr) (struct st40_rfc8331_payload_hdr*)(&hdr[1]); int anc_count = hdr->first_hdr_chunk.anc_count; - int idx, total_size, payload_len; + int idx, payload_len; dbg("%s(%d), anc_count %d\n", __func__, s->idx, anc_count); for (idx = 0; idx < anc_count; idx++) { - payload_hdr->swapped_first_hdr_chunk = ntohl(payload_hdr->swapped_first_hdr_chunk); - payload_hdr->swapped_second_hdr_chunk = ntohl(payload_hdr->swapped_second_hdr_chunk); + st40_rfc8331_payload_hdr_bswap(payload_hdr); if (!st40_check_parity_bits(payload_hdr->second_hdr_chunk.did) || !st40_check_parity_bits(payload_hdr->second_hdr_chunk.sdid) || !st40_check_parity_bits(payload_hdr->second_hdr_chunk.data_count)) { @@ -43,13 +42,7 @@ static void app_rx_anc_handle_rtp(struct st_app_rx_anc_session* s, void* usrptr) } dbg("\n"); #endif - total_size = ((3 + udw_size + 1) * 10) / 8; // Calculate size of the - // 10-bit words: DID, SDID, DATA_COUNT - // + size of buffer with data + checksum - total_size = (4 - total_size % 4) + total_size; // Calculate word align to the 32-bit - // word of ANC data packet - payload_len = - sizeof(struct st40_rfc8331_payload_hdr) - 4 + total_size; // Full size of one ANC + payload_len = st40_rfc8331_payload_bytes(udw_size); // Full size of one ANC payload_hdr = (struct st40_rfc8331_payload_hdr*)((uint8_t*)payload_hdr + payload_len); } @@ -150,6 +143,7 @@ static int app_rx_anc_init(struct st_app_context* ctx, st_json_ancillary_session anc ? anc->base.inf[MTL_SESSION_PORT_R]->name : ctx->para.port[MTL_PORT_R]); ops.udp_port[MTL_SESSION_PORT_R] = anc ? anc->base.udp_port : (10200 + s->idx); } + ops.type = ST40_TYPE_RTP_LEVEL; ops.rtp_ring_size = 1024; ops.payload_type = anc ? anc->base.payload_type : ST_APP_PAYLOAD_TYPE_ANCILLARY; ops.interlaced = anc ? anc->info.interlaced : false; diff --git a/tests/tools/RxTxApp/src/tx_ancillary_app.c b/tests/tools/RxTxApp/src/tx_ancillary_app.c index 98b53c196..86fbb21cd 100644 --- a/tests/tools/RxTxApp/src/tx_ancillary_app.c +++ b/tests/tools/RxTxApp/src/tx_ancillary_app.c @@ -202,7 +202,7 @@ static void app_tx_anc_build_rtp(struct st_app_tx_anc_session* s, void* usrptr, uint16_t udw_size = s->st40_source_end - s->st40_frame_cursor > 255 ? 255 : s->st40_source_end - s->st40_frame_cursor; - uint16_t check_sum, total_size, payload_len; + uint16_t check_sum, payload_len; hdr->base.marker = 1; hdr->first_hdr_chunk.anc_count = 1; hdr->base.payload_type = s->st40_payload_type; @@ -226,21 +226,14 @@ static void app_tx_anc_build_rtp(struct st_app_tx_anc_session* s, void* usrptr, payload_hdr->second_hdr_chunk.did = st40_add_parity_bits(0x43); payload_hdr->second_hdr_chunk.sdid = st40_add_parity_bits(0x02); payload_hdr->second_hdr_chunk.data_count = st40_add_parity_bits(udw_size); - payload_hdr->swapped_first_hdr_chunk = htonl(payload_hdr->swapped_first_hdr_chunk); - payload_hdr->swapped_second_hdr_chunk = htonl(payload_hdr->swapped_second_hdr_chunk); + st40_rfc8331_payload_hdr_bswap(payload_hdr); for (int i = 0; i < udw_size; i++) { st40_set_udw(i + 3, st40_add_parity_bits(s->st40_frame_cursor[i]), (uint8_t*)&payload_hdr->second_hdr_chunk); } check_sum = st40_calc_checksum(3 + udw_size, (uint8_t*)&payload_hdr->second_hdr_chunk); st40_set_udw(udw_size + 3, check_sum, (uint8_t*)&payload_hdr->second_hdr_chunk); - total_size = ((3 + udw_size + 1) * 10) / 8; // Calculate size of the - // 10-bit words: DID, SDID, DATA_COUNT - // + size of buffer with data + checksum - total_size = (4 - total_size % 4) + total_size; // Calculate word align to the 32-bit - // word of ANC data packet - payload_len = - sizeof(struct st40_rfc8331_payload_hdr) - 4 + total_size; // Full size of one ANC + payload_len = st40_rfc8331_payload_bytes(udw_size); // Full size of one ANC *mbuf_len = payload_len + sizeof(struct st40_rfc8331_rtp_hdr); hdr->length = htons(payload_len); diff --git a/tests/unit/README.md b/tests/unit/README.md index dd154d73c..457ac291f 100644 --- a/tests/unit/README.md +++ b/tests/unit/README.md @@ -35,6 +35,16 @@ in this binary. ## Quick start +```bash +./build.sh unit +``` + +This configures `build_unit/` with `-Denable_unit_tests=true`, builds it, +and runs `tests/unit/UnitTest` (preloading ASan automatically when combined +with `debug`, e.g. `./build.sh debug unit`). + +To configure/build/run manually, or to filter tests (see below): + ```bash # 1. configure (one-time) meson setup build_unit -Denable_unit_tests=true diff --git a/tests/unit/common/ut_common.c b/tests/unit/common/ut_common.c index 6d38661f0..5d9e948fb 100644 --- a/tests/unit/common/ut_common.c +++ b/tests/unit/common/ut_common.c @@ -6,6 +6,7 @@ #include "ut_common.h" +#include #include #include @@ -65,3 +66,16 @@ void ut_ring_drain(struct rte_ring* ring) { struct rte_mbuf* pkt = NULL; while (rte_ring_sc_dequeue(ring, (void**)&pkt) == 0) rte_pktmbuf_free(pkt); } + +/* ── HW RX timestamp mock ─────────────────────────────────────────────── */ + +int ut_register_hw_rx_timestamp(void) { + static int g_offset = -1; + if (g_offset < 0 && rte_mbuf_dyn_rx_timestamp_register(&g_offset, NULL) < 0) return -1; + return g_offset; +} + +void ut_mbuf_set_hw_timestamp(struct rte_mbuf* mbuf, int dynfield_offset, + uint64_t raw_ns) { + *RTE_MBUF_DYNFIELD(mbuf, dynfield_offset, rte_mbuf_timestamp_t*) = raw_ns; +} diff --git a/tests/unit/common/ut_common.h b/tests/unit/common/ut_common.h index 602cbaa80..7338cbeeb 100644 --- a/tests/unit/common/ut_common.h +++ b/tests/unit/common/ut_common.h @@ -37,6 +37,14 @@ struct rte_ring* ut_ring_create(const char* name, unsigned int size); /** Drain all mbufs from a ring and free them. */ void ut_ring_drain(struct rte_ring* ring); +/** Register (once) the real DPDK RX-timestamp mbuf dynfield used by + * mt_mbuf_time_stamp()'s HW path. Returns the dynfield offset. */ +int ut_register_hw_rx_timestamp(void); + +/** Stamp mbuf's HW RX-timestamp dynfield with a raw nanosecond value. */ +void ut_mbuf_set_hw_timestamp(struct rte_mbuf* mbuf, int dynfield_offset, + uint64_t raw_ns); + #ifdef __cplusplus } #endif diff --git a/tests/unit/meson.build b/tests/unit/meson.build index 268b4bef3..94135cda9 100644 --- a/tests/unit/meson.build +++ b/tests/unit/meson.build @@ -18,6 +18,7 @@ libpthread_dep = meson.get_compiler('c').find_library('pthread', required : true unit_sources = [ 'common/ut_common.c', 'session/st40_harness.c', + 'session/st40_tx_test_harness.c', 'session/st40/redundancy_test.cpp', 'session/st40/timestamp_test.cpp', 'session/st40/header_validation_test.cpp', @@ -28,6 +29,10 @@ unit_sources = [ 'session/st40/prev_window_test.cpp', 'session/st40/bitmap_test.cpp', 'session/st40/err_packets_test.cpp', + 'session/st40/frame_assembly_test.cpp', + 'session/st40/rfc8331_payload_bytes_test.cpp', + 'session/st40/rfc8331_codec_test.cpp', + 'session/st40/tx_corrupt_parity_test.cpp', 'session/st30_harness.c', 'session/st30/redundancy_test.cpp', 'session/st30/timestamp_test.cpp', @@ -44,12 +49,13 @@ unit_sources = [ 'session/st20/reorder_test.cpp', 'session/st20/stats_test.cpp', 'session/st20/err_packets_test.cpp', - 'pipeline/st40p_harness.c', - 'pipeline/st40p_test.cpp', + 'session/st20/timestamp_source_test.cpp', 'pipeline/st20p_harness.c', 'pipeline/st20p_test.cpp', 'pipeline/st30p_harness.c', 'pipeline/st30p_test.cpp', + 'pipeline/st40p_harness.c', + 'pipeline/st40p_test.cpp', 'main.cpp', ] diff --git a/tests/unit/pipeline/st20p_harness.c b/tests/unit/pipeline/st20p_harness.c index 863fc511b..e5412544e 100644 --- a/tests/unit/pipeline/st20p_harness.c +++ b/tests/unit/pipeline/st20p_harness.c @@ -18,6 +18,10 @@ #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 @@ -29,21 +33,19 @@ #include "st2110/pipeline/st20_pipeline_rx.c" #pragma GCC diagnostic pop -#include "common/ut_common.h" +#undef st20_rx_get_session_stats +#undef st20_rx_put_framebuff +#undef st20_rx_reset_session_stats -/* - * 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). --allow-multiple-definition lets ours win. - */ +#include "common/ut_common.h" -int st20_rx_put_framebuff(st20_rx_handle handle, void* frame) { +int ut20p_rx_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 st20_rx_get_session_stats(st20_rx_handle handle, struct st20_rx_user_stats* stats) { +int ut20p_rx_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. */ @@ -51,7 +53,7 @@ int st20_rx_get_session_stats(st20_rx_handle handle, struct st20_rx_user_stats* return 0; } -int st20_rx_reset_session_stats(st20_rx_handle handle) { +int ut20p_rx_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 608577e78..6c1b67534 100644 --- a/tests/unit/pipeline/st30p_harness.c +++ b/tests/unit/pipeline/st30p_harness.c @@ -10,29 +10,35 @@ #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 #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-variable" #include "st2110/pipeline/st30_pipeline_rx.c" #pragma GCC diagnostic pop -#include "common/ut_common.h" +#undef st30_rx_get_session_stats +#undef st30_rx_put_framebuff +#undef st30_rx_reset_session_stats -/* libmtl stubs */ +#include "common/ut_common.h" -int st30_rx_put_framebuff(st30_rx_handle handle, void* frame) { +int ut30p_rx_put_framebuff(st30_rx_handle handle, void* frame) { (void)handle; (void)frame; return 0; } -int st30_rx_get_session_stats(st30_rx_handle handle, struct st30_rx_user_stats* stats) { +int ut30p_rx_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 st30_rx_reset_session_stats(st30_rx_handle handle) { +int ut30p_rx_reset_session_stats(st30_rx_handle handle) { (void)handle; return 0; } diff --git a/tests/unit/pipeline/st40p_harness.c b/tests/unit/pipeline/st40p_harness.c index 2e4399b9d..6af49d05b 100644 --- a/tests/unit/pipeline/st40p_harness.c +++ b/tests/unit/pipeline/st40p_harness.c @@ -1,372 +1,138 @@ /* SPDX-License-Identifier: BSD-3-Clause - * Copyright(c) 2025 Intel Corporation + * Copyright(c) 2026 Intel Corporation * - * C harness for ST40 pipeline-layer unit tests. - * Tests rx_st40p_rtp_ready() — the frame-assembly callback that runs - * above the session-layer redundancy filter. + * C harness for ST40p (ancillary) pipeline-layer unit tests. + * Mirrors st30p_harness in spirit: drives rx_st40p_frame_ready() directly + * and stubs the transport-side st40_rx_put_framebuff() release call. */ -#include -#include -#include -#include -#include -#include #include #include -/* - * Include the production pipeline .c so that all static functions - * (rx_st40p_rtp_ready, rx_st40p_next_available, etc.) become visible. - * Disable USDT to avoid linker references to probe semaphores. - * Suppress -Wunused-variable for variables only used in USDT macros. - */ +#define st40_rx_get_session_stats ut40p_rx_get_session_stats +#define st40_rx_put_framebuff ut40p_rx_put_framebuff +#define st40_rx_reset_session_stats ut40p_rx_reset_session_stats + #undef MTL_HAS_USDT #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-variable" #include "st2110/pipeline/st40_pipeline_rx.c" #pragma GCC diagnostic pop -/* - * Provide stubs for symbols called by rx_st40p_rtp_ready() that would - * otherwise resolve to libmtl.so (which requires real HW / hugepages). - * With --allow-multiple-definition these override the libmtl versions. - */ +#undef st40_rx_get_session_stats +#undef st40_rx_put_framebuff +#undef st40_rx_reset_session_stats + +#include "common/ut_common.h" + +static int g_put_framebuff_calls; +static void* g_put_framebuff_last_addr; -uint64_t mt_mbuf_time_stamp(struct mtl_main_impl* impl, struct rte_mbuf* mbuf, - enum mtl_port port) { - (void)impl; - (void)mbuf; - (void)port; - return 0; /* HW timestamps are irrelevant for frame-assembly tests */ +int ut40p_rx_put_framebuff(st40_rx_handle handle, void* frame) { + (void)handle; + g_put_framebuff_calls++; + g_put_framebuff_last_addr = frame; + return 0; } -#include "common/ut_common.h" +int ut40p_put_framebuff_call_count(void) { + return g_put_framebuff_calls; +} -/* ── constants ────────────────────────────────────────────────────────── */ +void* ut40p_put_framebuff_last_addr(void) { + return g_put_framebuff_last_addr; +} -#define UT40P_RING_SIZE 512 -#define UT40P_UDW_BUFF_SIZE 4096 -/* L2+L3+L4 header sizes — must match st40_rx_get_mbuf skip logic */ -#define UT40P_L234_HDR_LEN \ - (sizeof(struct rte_ether_hdr) + sizeof(struct rte_ipv4_hdr) + \ - sizeof(struct rte_udp_hdr)) +void ut40p_put_framebuff_reset_spy(void) { + g_put_framebuff_calls = 0; + g_put_framebuff_last_addr = NULL; +} -/* ── opaque context ───────────────────────────────────────────────────── */ +int ut40p_rx_get_session_stats(st40_rx_handle handle, struct st40_rx_user_stats* stats) { + (void)handle; + if (stats) memset(stats, 0, sizeof(*stats)); + return 0; +} + +int ut40p_rx_reset_session_stats(st40_rx_handle handle) { + (void)handle; + return 0; +} struct ut40p_ctx { - /* mock impl — just needs type for sanity checks */ struct mtl_main_impl impl; - - /* mock transport: handle → session → packet_ring */ - struct st_rx_ancillary_session_handle_impl handle; - struct st_rx_ancillary_session_impl session; - /* mock sessions mgr — only mutex[idx] is touched by the transport stats path. */ - struct st_rx_ancillary_sessions_mgr session_mgr; - - /* the pipeline context under test */ struct st40p_rx_ctx pipeline; - - /* frame buffers allocated by us (not via mt_rte_zmalloc) */ struct st40p_rx_frame* framebuffs; - uint8_t** udw_buffers; /* per-frame UDW buffers */ int framebuff_cnt; }; #include "pipeline/st40p_harness.h" -/* ── globals ──────────────────────────────────────────────────────────── */ - -static struct rte_ring* g_mock_ring; - -/* ── init ─────────────────────────────────────────────────────────────── */ - int ut40p_init(void) { - if (ut_eal_init() < 0) return -1; - - if (!g_mock_ring) { - g_mock_ring = ut_ring_create("st40p_ring", UT40P_RING_SIZE); - if (!g_mock_ring) return -1; - } - return 0; + return ut_eal_init(); } -/* ── context create / destroy ─────────────────────────────────────────── */ - -ut40p_ctx* ut40p_ctx_create(int num_port, int framebuff_cnt) { +ut40p_ctx* ut40p_ctx_create(int framebuff_cnt) { ut40p_ctx* ctx = calloc(1, sizeof(*ctx)); if (!ctx) return NULL; ctx->framebuff_cnt = framebuff_cnt; - - /* minimal impl */ ctx->impl.type = MT_HANDLE_MAIN; - /* mock session: idx, packet_ring, mgr (with a live spinlock) */ - ctx->session.idx = 0; - ctx->session.packet_ring = g_mock_ring; - rte_spinlock_init(&ctx->session_mgr.mutex[0]); - ctx->session.mgr = &ctx->session_mgr; - - /* mock transport handle */ - ctx->handle.type = MT_HANDLE_RX_ANC; - ctx->handle.impl = &ctx->session; - - /* allocate frame buffers */ ctx->framebuffs = calloc(framebuff_cnt, sizeof(struct st40p_rx_frame)); - ctx->udw_buffers = calloc(framebuff_cnt, sizeof(uint8_t*)); - if (!ctx->framebuffs || !ctx->udw_buffers) { - ut40p_ctx_destroy(ctx); + if (!ctx->framebuffs) { + free(ctx); return NULL; } - for (int i = 0; i < framebuff_cnt; i++) { - struct st40p_rx_frame* fb = &ctx->framebuffs[i]; - fb->stat = ST40P_RX_FRAME_FREE; - fb->idx = i; - fb->frame_info.meta = fb->meta; - fb->frame_info.priv = fb; - - ctx->udw_buffers[i] = calloc(1, UT40P_UDW_BUFF_SIZE); - if (!ctx->udw_buffers[i]) { - ut40p_ctx_destroy(ctx); - return NULL; - } - fb->frame_info.udw_buff_addr = ctx->udw_buffers[i]; - fb->frame_info.udw_buffer_size = UT40P_UDW_BUFF_SIZE; + ctx->framebuffs[i].stat = ST40P_RX_FRAME_FREE; + ctx->framebuffs[i].idx = i; + ctx->framebuffs[i].frame_info.priv = &ctx->framebuffs[i]; + ctx->framebuffs[i].frame_info.meta = ctx->framebuffs[i].meta; } - /* pipeline context */ struct st40p_rx_ctx* p = &ctx->pipeline; p->impl = &ctx->impl; p->idx = 0; p->socket_id = rte_socket_id(); p->type = MT_ST40_HANDLE_PIPELINE_RX; - - p->transport = (st40_rx_handle)&ctx->handle; p->framebuff_cnt = framebuff_cnt; - p->framebuff_producer_idx = 0; - p->framebuff_consumer_idx = 0; p->framebuffs = ctx->framebuffs; - p->inflight_frame = NULL; - - p->ops.port.num_port = num_port; - - /* port mapping: session port 0 → DPDK port id 0, session port 1 → 1 */ - p->port_map[MTL_SESSION_PORT_P] = MTL_PORT_P; - p->port_map[MTL_SESSION_PORT_R] = MTL_PORT_R; - p->port_id[MTL_SESSION_PORT_P] = 0; - p->port_id[MTL_SESSION_PORT_R] = 1; - p->ready = true; - if (pthread_mutex_init(&p->lock, NULL) != 0) { - ut40p_ctx_destroy(ctx); + p->transport = (st40_rx_handle)(uintptr_t)0x1; + + if (mt_pthread_mutex_init(&p->lock, NULL) != 0) { + free(ctx->framebuffs); + free(ctx); return NULL; } - /* drain any stale mbufs from a previous test */ - ut_ring_drain(g_mock_ring); + ut40p_put_framebuff_reset_spy(); return ctx; } void ut40p_ctx_destroy(ut40p_ctx* ctx) { if (!ctx) return; - - ut_ring_drain(g_mock_ring); pthread_mutex_destroy(&ctx->pipeline.lock); - - if (ctx->udw_buffers) { - for (int i = 0; i < ctx->framebuff_cnt; i++) free(ctx->udw_buffers[i]); - free(ctx->udw_buffers); - } free(ctx->framebuffs); free(ctx); } -/* ── mbuf builder ─────────────────────────────────────────────────────── */ - -/* - * Build an mbuf with L2+L3+L4 prefix followed by an RFC 8331 RTP header - * with anc_count=0 (no ANC payload), for testing frame assembly. - * - * Layout: - * [rte_ether_hdr][rte_ipv4_hdr][rte_udp_hdr][st40_rfc8331_rtp_hdr][payload_hdr] - */ -static struct rte_mbuf* make_pipeline_mbuf(uint16_t seq, uint32_t ts, int marker, - uint16_t dpdk_port_id) { - struct rte_mbuf* m = rte_pktmbuf_alloc(ut_pool()); - if (!m) return NULL; - - /* anc_count=0: no ANC payload, simplifies frame-assembly-only tests. */ - size_t rtp_len = sizeof(struct st40_rfc8331_rtp_hdr); - size_t total = UT40P_L234_HDR_LEN + rtp_len; - - if (rte_pktmbuf_tailroom(m) < total) { - rte_pktmbuf_free(m); - return NULL; - } +int ut40p_inject_frame(ut40p_ctx* ctx, void* udw_addr, enum st_frame_status status, + bool seq_discont, uint32_t timestamp) { + struct st40_rx_frame_meta meta; + memset(&meta, 0, sizeof(meta)); + meta.timestamp = timestamp; + meta.rtp_timestamp = timestamp; + meta.status = status; + meta.seq_discont = seq_discont; + meta.meta_num = 0; + meta.meta = NULL; - uint8_t* buf = rte_pktmbuf_mtod(m, uint8_t*); - memset(buf, 0, total); - - /* RTP header starts after L2+L3+L4 */ - struct st40_rfc8331_rtp_hdr* rtp = - (struct st40_rfc8331_rtp_hdr*)(buf + UT40P_L234_HDR_LEN); - rtp->base.version = 2; - rtp->base.seq_number = htons(seq); - rtp->base.tmstamp = htonl(ts); - rtp->base.marker = marker ? 1 : 0; - - /* anc_count = 0: no ANC payload to parse, simplifies frame-assembly tests */ - uint32_t chunk = 0; /* anc_count=0, f=0b00 (progressive) */ - rtp->swapped_first_hdr_chunk = htonl(chunk); - - m->data_len = total; - m->pkt_len = total; - m->port = dpdk_port_id; - return m; + return rx_st40p_frame_ready(&ctx->pipeline, udw_addr, &meta); } -static uint32_t ut40p_anc_payload_bytes(uint16_t udw_size) { - uint32_t total_bits = (uint32_t)(3 + udw_size + 1) * 10; - uint32_t total_size = (total_bits + 7) / 8; - total_size = (total_size + 3) & ~0x3U; - return sizeof(struct st40_rfc8331_payload_hdr) - 4 + total_size; -} - -static struct rte_mbuf* make_multi_anc_mbuf(uint16_t seq, uint32_t ts, int marker, - uint16_t dpdk_port_id, - const uint16_t* udw_sizes, - uint8_t anc_count) { - if (!udw_sizes || !anc_count || anc_count > ST40_MAX_META) return NULL; - - size_t payload_len = 0; - for (uint8_t anc_idx = 0; anc_idx < anc_count; anc_idx++) - payload_len += ut40p_anc_payload_bytes(udw_sizes[anc_idx]); - - size_t rtp_len = sizeof(struct st40_rfc8331_rtp_hdr) + payload_len; - size_t total = UT40P_L234_HDR_LEN + rtp_len; - - struct rte_mbuf* m = rte_pktmbuf_alloc(ut_pool()); - if (!m) return NULL; - - if (rte_pktmbuf_tailroom(m) < total) { - rte_pktmbuf_free(m); - return NULL; - } - - uint8_t* buf = rte_pktmbuf_mtod(m, uint8_t*); - memset(buf, 0, total); - - struct st40_rfc8331_rtp_hdr* rtp = - (struct st40_rfc8331_rtp_hdr*)(buf + UT40P_L234_HDR_LEN); - rtp->base.version = 2; - rtp->base.seq_number = htons(seq); - rtp->base.tmstamp = htonl(ts); - rtp->base.marker = marker ? 1 : 0; - rtp->length = htons(payload_len); - rtp->first_hdr_chunk.anc_count = anc_count; - rtp->first_hdr_chunk.f = 0b00; - /* The RTP first_hdr_chunk is host-order on entry to the pipeline: the - * session layer byte-swaps it in place before passing the mbuf up. The - * per-ANC payload headers below stay in network order; the pipeline - * ntohl()'s them itself. */ - - uint8_t* payload = (uint8_t*)(rtp + 1); - for (uint8_t anc_idx = 0; anc_idx < anc_count; anc_idx++) { - uint16_t udw_size = udw_sizes[anc_idx]; - struct st40_rfc8331_payload_hdr* payload_hdr = - (struct st40_rfc8331_payload_hdr*)payload; - - payload_hdr->first_hdr_chunk.c = 0; - payload_hdr->first_hdr_chunk.line_number = 10 + anc_idx; - payload_hdr->first_hdr_chunk.horizontal_offset = 0; - payload_hdr->first_hdr_chunk.s = 0; - payload_hdr->first_hdr_chunk.stream_num = 0; - payload_hdr->second_hdr_chunk.did = st40_add_parity_bits(0x45); - payload_hdr->second_hdr_chunk.sdid = st40_add_parity_bits(0x01); - payload_hdr->second_hdr_chunk.data_count = st40_add_parity_bits(udw_size); - - payload_hdr->swapped_first_hdr_chunk = htonl(payload_hdr->swapped_first_hdr_chunk); - payload_hdr->swapped_second_hdr_chunk = htonl(payload_hdr->swapped_second_hdr_chunk); - - uint8_t* udw_dst = (uint8_t*)&payload_hdr->second_hdr_chunk; - for (uint16_t udw_idx = 0; udw_idx < udw_size; udw_idx++) { - uint8_t v = (uint8_t)(((uint16_t)(anc_idx + 1) * 17 + udw_idx) & 0xff); - st40_set_udw(udw_idx + 3, st40_add_parity_bits(v), udw_dst); - } - uint16_t checksum = st40_calc_checksum(3 + udw_size, udw_dst); - st40_set_udw(udw_size + 3, checksum, udw_dst); - - payload += ut40p_anc_payload_bytes(udw_size); - } - - m->data_len = total; - m->pkt_len = total; - m->port = dpdk_port_id; - return m; -} - -/* ── enqueue functions ────────────────────────────────────────────────── */ - -int ut40p_enqueue_pkt(ut40p_ctx* ctx, uint16_t seq, uint32_t ts, int marker, - enum mtl_session_port port) { - uint16_t dpdk_port_id = (port == MTL_SESSION_PORT_R) ? 1 : 0; - return ut40p_enqueue_pkt_port_id(ctx, seq, ts, marker, dpdk_port_id); -} - -int ut40p_enqueue_pkt_port_id(ut40p_ctx* ctx, uint16_t seq, uint32_t ts, int marker, - uint16_t dpdk_port_id) { - (void)ctx; - struct rte_mbuf* m = make_pipeline_mbuf(seq, ts, marker, dpdk_port_id); - if (!m) return -1; - - if (rte_ring_sp_enqueue(g_mock_ring, m) != 0) { - rte_pktmbuf_free(m); - return -1; - } - return 0; -} - -void ut40p_enqueue_burst(ut40p_ctx* ctx, uint16_t seq_start, int count, uint32_t ts, - int last_marker, enum mtl_session_port port) { - for (int i = 0; i < count; i++) { - int marker = last_marker && (i == count - 1); - ut40p_enqueue_pkt(ctx, seq_start + i, ts, marker, port); - } -} - -int ut40p_enqueue_multi_anc_pkt(ut40p_ctx* ctx, uint16_t seq, uint32_t ts, int marker, - enum mtl_session_port port, const uint16_t* udw_sizes, - uint8_t anc_count) { - (void)ctx; - uint16_t dpdk_port_id = (port == MTL_SESSION_PORT_R) ? 1 : 0; - struct rte_mbuf* m = - make_multi_anc_mbuf(seq, ts, marker, dpdk_port_id, udw_sizes, anc_count); - if (!m) return -1; - - if (rte_ring_sp_enqueue(g_mock_ring, m) != 0) { - rte_pktmbuf_free(m); - return -1; - } - return 0; -} - -/* ── process functions ────────────────────────────────────────────────── */ - -int ut40p_process(ut40p_ctx* ctx) { - return rx_st40p_rtp_ready(&ctx->pipeline); -} - -void ut40p_process_all(ut40p_ctx* ctx) { - while (rx_st40p_rtp_ready(&ctx->pipeline) == 0) { - /* keep processing until ring is empty or error */ - } -} - -/* ── frame get/put ────────────────────────────────────────────────────── */ - struct st40_frame_info* ut40p_get_frame(ut40p_ctx* ctx) { return st40p_rx_get_frame(&ctx->pipeline); } @@ -375,14 +141,8 @@ int ut40p_put_frame(ut40p_ctx* ctx, struct st40_frame_info* frame) { return st40p_rx_put_frame(&ctx->pipeline, frame); } -/* ── stat accessors ───────────────────────────────────────────────────── */ - -uint32_t ut40p_stat_busy(const ut40p_ctx* ctx) { - return ctx->pipeline.stat_busy; -} - -uint32_t ut40p_stat_drop_frame(const ut40p_ctx* ctx) { - return ctx->pipeline.stat_drop_frame; +int ut40p_put_frame_abort(ut40p_ctx* ctx, struct st40_frame_info* frame) { + return st40p_rx_put_frame_abort(&ctx->pipeline, frame); } uint64_t ut40p_stat_frames_received(const ut40p_ctx* ctx) { @@ -397,6 +157,10 @@ uint64_t ut40p_stat_frames_corrupted(const ut40p_ctx* ctx) { return ctx->pipeline.stat_frames_corrupted; } +uint32_t ut40p_stat_busy(const ut40p_ctx* ctx) { + return ctx->pipeline.stat_busy; +} + int ut40p_get_session_stats(ut40p_ctx* ctx, struct st40_rx_user_stats* stats) { return st40p_rx_get_session_stats(&ctx->pipeline, stats); } diff --git a/tests/unit/pipeline/st40p_harness.h b/tests/unit/pipeline/st40p_harness.h index 32ad96be8..8c1f2e2f7 100644 --- a/tests/unit/pipeline/st40p_harness.h +++ b/tests/unit/pipeline/st40p_harness.h @@ -1,8 +1,16 @@ /* SPDX-License-Identifier: BSD-3-Clause - * Copyright(c) 2025 Intel Corporation + * Copyright(c) 2026 Intel Corporation * - * C header for ST40 pipeline-layer unit tests. - * Tests the frame-assembly logic in rx_st40p_rtp_ready(). + * C header for ST40p (ancillary) pipeline-layer unit tests. + * + * Pins the pipeline-internal FRAME_LEVEL zero-copy contract: + * - stat_frames_received / stat_frames_corrupted bump in + * rx_st40p_frame_ready() (transport delivery), NOT in get_frame() — + * unlike ST30p, where the app's get_frame call is the counting point. + * - stat_frames_dropped / stat_busy bump 1:1 in rx_st40p_frame_ready() + * when no free framebuf is available (back-pressure). + * - put_frame / put_frame_abort release the transport-owned UDW slot via + * st40_rx_put_framebuff() and must clear frame_info->udw_buff_addr. */ #ifndef _ST40P_PIPELINE_HARNESS_H_ @@ -22,58 +30,31 @@ typedef struct ut40p_ctx ut40p_ctx; int ut40p_init(void); -/** Create a pipeline test context. - * num_port: 1 or 2 (redundant). - * framebuff_cnt: number of frame buffers (3 is typical). */ -ut40p_ctx* ut40p_ctx_create(int num_port, int framebuff_cnt); +ut40p_ctx* ut40p_ctx_create(int framebuff_cnt); void ut40p_ctx_destroy(ut40p_ctx* ctx); -/** Enqueue one ANC RTP packet into the mock transport ring. - * The mbuf is built with L2+L3+L4 headers followed by an RFC 8331 RTP header - * with anc_count=0 (no ANC payload). - * port: MTL_SESSION_PORT_P or MTL_SESSION_PORT_R. */ -int ut40p_enqueue_pkt(ut40p_ctx* ctx, uint16_t seq, uint32_t ts, int marker, - enum mtl_session_port port); - -/** Enqueue one ANC RTP packet with a custom DPDK port_id (for unmapped port tests). */ -int ut40p_enqueue_pkt_port_id(ut40p_ctx* ctx, uint16_t seq, uint32_t ts, int marker, - uint16_t dpdk_port_id); - -/** Enqueue a burst of sequential packets. marker on last if last_marker. */ -void ut40p_enqueue_burst(ut40p_ctx* ctx, uint16_t seq_start, int count, uint32_t ts, - int last_marker, enum mtl_session_port port); - -/** Enqueue one RTP packet carrying multiple RFC 8331 ANC data packets. */ -int ut40p_enqueue_multi_anc_pkt(ut40p_ctx* ctx, uint16_t seq, uint32_t ts, int marker, - enum mtl_session_port port, const uint16_t* udw_sizes, - uint8_t anc_count); - -/** Call rx_st40p_rtp_ready() once — processes one mbuf from the ring. */ -int ut40p_process(ut40p_ctx* ctx); - -/** Call rx_st40p_rtp_ready() in a loop until the ring is empty. */ -void ut40p_process_all(ut40p_ctx* ctx); +/** Inject one synthetic ANC frame as if the transport just completed it. + * udw_addr is any non-NULL sentinel pointer identifying the transport slot. + * Returns 0 on accept, -EBUSY when no free framebuf (drives the + * stat_frames_dropped path). */ +int ut40p_inject_frame(ut40p_ctx* ctx, void* udw_addr, enum st_frame_status status, + bool seq_discont, uint32_t timestamp); -/** Get next READY frame (wraps st40p_rx_get_frame). Returns NULL if none. */ struct st40_frame_info* ut40p_get_frame(ut40p_ctx* ctx); - -/** Return a frame to the pipeline (wraps st40p_rx_put_frame). */ int ut40p_put_frame(ut40p_ctx* ctx, struct st40_frame_info* frame); +int ut40p_put_frame_abort(ut40p_ctx* ctx, struct st40_frame_info* frame); -/* ── stat accessors ───────────────────────────────────────────────── */ - -uint32_t ut40p_stat_busy(const ut40p_ctx* ctx); -uint32_t ut40p_stat_drop_frame(const ut40p_ctx* ctx); uint64_t ut40p_stat_frames_received(const ut40p_ctx* ctx); uint64_t ut40p_stat_frames_dropped(const ut40p_ctx* ctx); uint64_t ut40p_stat_frames_corrupted(const ut40p_ctx* ctx); +uint32_t ut40p_stat_busy(const ut40p_ctx* ctx); -/* ── public-API wrappers ─────────────────────────────────────────── */ +/** put_framebuff() release-wiring spy: what the stub last saw. */ +int ut40p_put_framebuff_call_count(void); +void* ut40p_put_framebuff_last_addr(void); +void ut40p_put_framebuff_reset_spy(void); -/** Wraps st40p_rx_get_session_stats() — exercises the atomic overlay path. */ int ut40p_get_session_stats(ut40p_ctx* ctx, struct st40_rx_user_stats* stats); - -/** Wraps st40p_rx_reset_session_stats() — exercises the atomic reset path. */ int ut40p_reset_session_stats(ut40p_ctx* ctx); #ifdef __cplusplus diff --git a/tests/unit/pipeline/st40p_test.cpp b/tests/unit/pipeline/st40p_test.cpp index 36547bfa4..167350dd8 100644 --- a/tests/unit/pipeline/st40p_test.cpp +++ b/tests/unit/pipeline/st40p_test.cpp @@ -1,14 +1,18 @@ /* SPDX-License-Identifier: BSD-3-Clause - * Copyright(c) 2025 Intel Corporation + * Copyright(c) 2026 Intel Corporation * - * Unit tests for ST2110-40 (ancillary) RX pipeline — frame assembly layer. - * Tests rx_st40p_rtp_ready() which sits above the session-layer redundancy - * filter and assembles RTP packets into frames. + * ST40p (ancillary) pipeline-layer zero-copy dispatch + release-wiring tests. + * + * stat_frames_received / stat_frames_corrupted -> bumped in + * rx_st40p_frame_ready() (transport delivery), not in get_frame(). + * stat_frames_dropped / stat_busy -> bump 1:1 in rx_st40p_frame_ready() + * when no free framebuf is available (back-pressure). + * put_frame / put_frame_abort -> release the transport-owned UDW slot via + * st40_rx_put_framebuff() and clear frame_info->udw_buff_addr. */ #include -#include "mtl_api.h" #include "pipeline/st40p_harness.h" class St40PipelineRxTest : public ::testing::Test { @@ -17,7 +21,7 @@ class St40PipelineRxTest : public ::testing::Test { void SetUp() override { ASSERT_EQ(ut40p_init(), 0) << "EAL init failed"; - ctx_ = ut40p_ctx_create(2, 3); /* 2 ports, 3 frame buffers */ + ctx_ = ut40p_ctx_create(/*framebuff_cnt=*/3); ASSERT_NE(ctx_, nullptr); } @@ -26,853 +30,141 @@ class St40PipelineRxTest : public ::testing::Test { ctx_ = nullptr; } - /* convenience wrappers */ - int enqueue(uint16_t seq, uint32_t ts, bool marker, enum mtl_session_port port) { - return ut40p_enqueue_pkt(ctx_, seq, ts, marker ? 1 : 0, port); - } - - void enqueue_burst(uint16_t seq_start, int count, uint32_t ts, bool last_marker, - enum mtl_session_port port) { - ut40p_enqueue_burst(ctx_, seq_start, count, ts, last_marker ? 1 : 0, port); - } - - int enqueue_multi_anc(uint16_t seq, uint32_t ts, bool marker, - enum mtl_session_port port, const uint16_t* udw_sizes, - uint8_t anc_count) { - return ut40p_enqueue_multi_anc_pkt(ctx_, seq, ts, marker ? 1 : 0, port, udw_sizes, - anc_count); + int inject(void* addr, uint32_t ts) { + return ut40p_inject_frame(ctx_, addr, ST_FRAME_STATUS_COMPLETE, false, ts); } - - int process() { - return ut40p_process(ctx_); - } - - void process_all() { - ut40p_process_all(ctx_); + int inject_discont(void* addr, uint32_t ts) { + return ut40p_inject_frame(ctx_, addr, ST_FRAME_STATUS_CORRUPTED, true, ts); } - struct st40_frame_info* get_frame() { return ut40p_get_frame(ctx_); } - int put_frame(struct st40_frame_info* f) { return ut40p_put_frame(ctx_, f); } - - uint32_t stat_busy() { - return ut40p_stat_busy(ctx_); + int put_frame_abort(struct st40_frame_info* f) { + return ut40p_put_frame_abort(ctx_, f); } - uint64_t stat_frames_received() { + uint64_t frames_received() { return ut40p_stat_frames_received(ctx_); } - uint64_t stat_frames_dropped() { + uint64_t frames_dropped() { return ut40p_stat_frames_dropped(ctx_); } - uint64_t stat_frames_corrupted() { + uint64_t frames_corrupted() { return ut40p_stat_frames_corrupted(ctx_); } -}; - -/* ── Normal pipeline operation ────────────────────────────────────────── */ - -/* Single complete frame: 6 packets with marker on last. */ -TEST_F(St40PipelineRxTest, SingleFrameCompletion) { - enqueue_burst(0, 6, 1000, true, MTL_SESSION_PORT_P); - process_all(); - - auto* frame = get_frame(); - ASSERT_NE(frame, nullptr); - EXPECT_EQ(frame->rtp_timestamp, 1000u); - EXPECT_EQ(frame->pkts_total, 6u); - EXPECT_TRUE(frame->rtp_marker); - EXPECT_EQ(frame->status, ST_FRAME_STATUS_COMPLETE); - put_frame(frame); -} - -/* Multiple ANC data packets carried in a single RTP packet (non-split mode). - * Validates that the receiver correctly walks the variable-length per-ANC - * payload stride when the 10-bit UDW packing of one or more blocks is not - * byte-aligned, and recovers the original metadata and user data words for - * every ANC packet in the frame. - * - * Two size patterns are exercised because a wrong stride manifests - * differently depending on what the misread bytes decode to: - * {8, 6, 4} — the misread data_count lands on zero padding, so the - * receiver silently produces an empty meta entry. Caught - * here by the per-byte UDW equality check. - * {6, 6, 6} — the misread data_count decodes to a large value that - * overflows the remaining payload room, surfacing as the - * "ANC packet bytes exceed payload" path and a meta_num - * short of anc_count. */ -TEST_F(St40PipelineRxTest, MultiAncInSingleRtpPacket) { - const uint16_t patterns[][3] = {{8, 6, 4}, {6, 6, 6}}; - const uint8_t anc_count = 3; - uint32_t ts = 1000; - - for (const auto& udw_sizes : patterns) { - ASSERT_EQ(enqueue_multi_anc(0, ts, true, MTL_SESSION_PORT_P, udw_sizes, anc_count), - 0); - process_all(); - - auto* frame = get_frame(); - ASSERT_NE(frame, nullptr); - EXPECT_EQ(frame->rtp_timestamp, ts); - EXPECT_EQ(frame->pkts_total, 1u); - EXPECT_TRUE(frame->rtp_marker); - EXPECT_EQ(frame->status, ST_FRAME_STATUS_COMPLETE); - ASSERT_EQ(frame->meta_num, anc_count); - - uint16_t expected_offset = 0; - for (uint8_t anc_idx = 0; anc_idx < anc_count; anc_idx++) { - const auto& meta = frame->meta[anc_idx]; - EXPECT_EQ(meta.c, 0u); - EXPECT_EQ(meta.line_number, 10u + anc_idx); - EXPECT_EQ(meta.hori_offset, 0u); - EXPECT_EQ(meta.s, 0u); - EXPECT_EQ(meta.stream_num, 0u); - EXPECT_EQ(meta.did, 0x45u); - EXPECT_EQ(meta.sdid, 0x01u); - EXPECT_EQ(meta.udw_size, udw_sizes[anc_idx]); - EXPECT_EQ(meta.udw_offset, expected_offset); - - for (uint16_t udw_idx = 0; udw_idx < udw_sizes[anc_idx]; udw_idx++) { - uint8_t expected = static_cast(((anc_idx + 1) * 17 + udw_idx) & 0xff); - EXPECT_EQ(frame->udw_buff_addr[expected_offset + udw_idx], expected); - } - - expected_offset += udw_sizes[anc_idx]; - } - EXPECT_EQ(frame->udw_buffer_fill, expected_offset); - put_frame(frame); - - ts += 1000; - } -} - -/* Timestamp change completes the previous frame without marker. */ -TEST_F(St40PipelineRxTest, TimestampChangeCompletesFrame) { - /* Single-port: ts change → immediate READY (no PENDING) */ - ut40p_ctx_destroy(ctx_); - ctx_ = ut40p_ctx_create(1, 3); - - /* frame 1: 3 pkts, no marker */ - enqueue_burst(0, 3, 1000, false, MTL_SESSION_PORT_P); - /* frame 2: 1 pkt with new ts → completes frame 1 */ - enqueue(3, 2000, false, MTL_SESSION_PORT_P); - process_all(); - - auto* frame1 = get_frame(); - ASSERT_NE(frame1, nullptr); - EXPECT_EQ(frame1->rtp_timestamp, 1000u); - EXPECT_EQ(frame1->pkts_total, 3u); - EXPECT_FALSE(frame1->rtp_marker) - << "Frame completed by ts change should not have marker"; - put_frame(frame1); -} - -/* Fill all frame buffers — additional packets should increment stat_busy. */ -TEST_F(St40PipelineRxTest, FramebufferExhaustion) { - /* 3 framebuffs: fill all with inflight frames via ts changes. - * ts=1000 → ts=2000 (completes frame 1, starts frame 2) - * ts=2000 → ts=3000 (completes frame 2, starts frame 3) - * All 3 frames are READY or RECEIVING; no free buffers. */ - enqueue(0, 1000, false, MTL_SESSION_PORT_P); - enqueue(1, 2000, false, MTL_SESSION_PORT_P); /* completes ts=1000 */ - enqueue(2, 3000, false, MTL_SESSION_PORT_P); /* completes ts=2000 */ - process_all(); - - /* frames 1 and 2 are READY, frame 3 is RECEIVING (inflight). - * No free framebuffs. Next ts change will try to allocate but fail. */ - enqueue(3, 4000, false, MTL_SESSION_PORT_P); /* completes ts=3000 → no free fb */ - process_all(); - - EXPECT_GE(stat_busy(), 1u) << "Should hit framebuffer exhaustion"; - /* Every stat_busy hit must also surface as stat_frames_dropped so the - * back-pressure is visible via session stats. */ - EXPECT_EQ(stat_frames_dropped(), stat_busy()); -} - -/* Sequence discontinuity tracking: gap between seq 1 and 3. */ -TEST_F(St40PipelineRxTest, SequenceDiscontinuity) { - enqueue(0, 1000, false, MTL_SESSION_PORT_P); - enqueue(1, 1000, false, MTL_SESSION_PORT_P); - /* skip seq 2 */ - enqueue(3, 1000, true, MTL_SESSION_PORT_P); - process_all(); - - auto* frame = get_frame(); - ASSERT_NE(frame, nullptr); - EXPECT_TRUE(frame->seq_discont); - EXPECT_EQ(frame->seq_lost, 1u); - put_frame(frame); -} - -/* Per-port sequence tracking: P has gap, R does not. */ -TEST_F(St40PipelineRxTest, PerPortSequenceTracking) { - enqueue(0, 1000, false, MTL_SESSION_PORT_P); - /* P skips seq 1 */ - enqueue(2, 1000, false, MTL_SESSION_PORT_P); - /* R fills in seq 1 */ - enqueue(1, 1000, false, MTL_SESSION_PORT_R); - enqueue(3, 1000, true, MTL_SESSION_PORT_R); - process_all(); - - auto* frame = get_frame(); - ASSERT_NE(frame, nullptr); - EXPECT_TRUE(frame->port_seq_discont[MTL_SESSION_PORT_P]); - EXPECT_EQ(frame->port_seq_lost[MTL_SESSION_PORT_P], 1u); - /* R sees seq 1 then 3 — gap of 1 too (since per-port tracks independently) */ - EXPECT_TRUE(frame->port_seq_discont[MTL_SESSION_PORT_R]); - EXPECT_EQ(frame->pkts_recv[MTL_SESSION_PORT_P], 2u); - EXPECT_EQ(frame->pkts_recv[MTL_SESSION_PORT_R], 2u); - put_frame(frame); -} - -/* Multiple frames delivered in sequence. */ -TEST_F(St40PipelineRxTest, MultiFrameDelivery) { - for (uint32_t ts = 1000; ts <= 3000; ts += 1000) { - enqueue_burst(0 + (ts - 1000) / 1000 * 4, 4, ts, true, MTL_SESSION_PORT_P); - } - process_all(); - - for (uint32_t ts = 1000; ts <= 3000; ts += 1000) { - auto* frame = get_frame(); - ASSERT_NE(frame, nullptr) << "Frame ts=" << ts << " should be available"; - EXPECT_EQ(frame->rtp_timestamp, ts); - EXPECT_TRUE(frame->rtp_marker); - put_frame(frame); + uint32_t stat_busy() { + return ut40p_stat_busy(ctx_); } +}; - /* no more frames */ - EXPECT_EQ(get_frame(), nullptr); -} - -/* Frame put recycles the buffer for reuse. */ -TEST_F(St40PipelineRxTest, FramePutRecyclesBuffer) { - /* fill and consume one frame */ - enqueue_burst(0, 4, 1000, true, MTL_SESSION_PORT_P); - process_all(); - auto* frame = get_frame(); - ASSERT_NE(frame, nullptr); - put_frame(frame); - - /* the returned slot should be reusable — send another frame */ - enqueue_burst(4, 4, 2000, true, MTL_SESSION_PORT_P); - process_all(); - frame = get_frame(); - ASSERT_NE(frame, nullptr); - EXPECT_EQ(frame->rtp_timestamp, 2000u); - put_frame(frame); -} - -/* Packet from unmapped port is dropped. */ -TEST_F(St40PipelineRxTest, UnmappedPortDropped) { - /* Enqueue a packet with port_id=99 (not mapped to any session port) */ - ASSERT_EQ(ut40p_enqueue_pkt_port_id(ctx_, 0, 1000, 1, 99), 0); - int rc = process(); - EXPECT_EQ(rc, -EIO); - EXPECT_EQ(get_frame(), nullptr) << "Unmapped port packet should not produce a frame"; -} - -/* ── RTP marker bit tests ─────────────────────────────────────────────── */ - -/* Redundant-path scenario: P sends body, R sends tail with marker, same timestamp. - * P sends seq 0-4 (no marker), R sends seq 5-6 (marker on 6). - * At the pipeline layer, both ports' packets arrive via the same packet_ring - * (session layer already deduplicated). Marker must survive. */ -TEST_F(St40PipelineRxTest, MarkerPreservedMidFrameSwitchover) { - /* P sends body: seq 0-4, no marker */ - for (int i = 0; i < 5; i++) enqueue(i, 1000, false, MTL_SESSION_PORT_P); - /* R sends tail: seq 5-6, marker on 6 */ - enqueue(5, 1000, false, MTL_SESSION_PORT_R); - enqueue(6, 1000, true, MTL_SESSION_PORT_R); - process_all(); - - auto* frame = get_frame(); - ASSERT_NE(frame, nullptr); - EXPECT_EQ(frame->rtp_timestamp, 1000u); - EXPECT_EQ(frame->pkts_total, 7u); - EXPECT_TRUE(frame->rtp_marker) - << "Marker from R port must survive mid-frame switchover at pipeline layer"; - put_frame(frame); -} - -/* Marker preservation on framebuffer exhaustion. - * - * When all framebuffers are occupied and a new timestamp arrives, the pipeline - * must still process the marker correctly when framebuffers become available. */ -TEST_F(St40PipelineRxTest, MarkerPreservedOnFramebufferExhaustion) { - /* Use single-port ctx with only 2 framebuffs to make exhaustion easier */ - ut40p_ctx_destroy(ctx_); - ctx_ = ut40p_ctx_create(1, 2); - ASSERT_NE(ctx_, nullptr); - - /* Frame 1: ts=1000, 3 pkts, no marker */ - enqueue_burst(0, 3, 1000, false, MTL_SESSION_PORT_P); - /* Frame 2: ts=2000 → completes frame 1, starts frame 2 */ - enqueue(3, 2000, false, MTL_SESSION_PORT_P); - process_all(); - - /* Now: frame 1 is READY (slot 0), frame 2 is RECEIVING (slot 1). - * Both framebuffers are occupied. */ - - /* Frame 3: ts=3000 → completes frame 2, but no free fb for frame 3 */ - enqueue(4, 3000, false, MTL_SESSION_PORT_P); - process_all(); - - /* Frame 2 is now READY (slot 1). Frame 3 hit stat_busy. - * The ts=3000 packet triggered a timestamp change, completing frame 2, - * but no framebuffer was available — the packet was dropped before - * marker processing. */ - - /* Now send a marker for ts=3000 — it also can't allocate a new fb */ - enqueue(5, 3000, true, MTL_SESSION_PORT_P); /* MARKER */ - process_all(); - - EXPECT_GE(stat_busy(), 1u); - - /* Frame 1 does not have marker (completed by ts change — expected) */ - auto* frame1 = get_frame(); - ASSERT_NE(frame1, nullptr); - EXPECT_EQ(frame1->rtp_timestamp, 1000u); - EXPECT_FALSE(frame1->rtp_marker); /* no marker in frame 1 — correct */ - put_frame(frame1); - - /* Frame 2 does not have marker either (completed by ts change — expected) */ - auto* frame2 = get_frame(); - ASSERT_NE(frame2, nullptr); - EXPECT_EQ(frame2->rtp_timestamp, 2000u); - EXPECT_FALSE(frame2->rtp_marker); /* no marker in frame 2 — correct */ - put_frame(frame2); - - /* Frame 3 was never assembled (all its packets hit stat_busy). - * Put frames 1 and 2 to free slots, then retry frame 3. */ - enqueue(10, 3000, false, MTL_SESSION_PORT_P); - enqueue(11, 3000, true, MTL_SESSION_PORT_P); - process_all(); - - auto* frame3 = get_frame(); - ASSERT_NE(frame3, nullptr); - EXPECT_EQ(frame3->rtp_timestamp, 3000u); - EXPECT_TRUE(frame3->rtp_marker) << "Frame 3 should have marker after retry"; - put_frame(frame3); -} - -/* Single-packet frame with marker. */ -TEST_F(St40PipelineRxTest, MarkerOnSinglePacketFrame) { - enqueue(0, 1000, true, MTL_SESSION_PORT_P); - process_all(); - - auto* frame = get_frame(); - ASSERT_NE(frame, nullptr); - EXPECT_EQ(frame->pkts_total, 1u); - EXPECT_TRUE(frame->rtp_marker); - put_frame(frame); -} - -/* Frame status is COMPLETE when no seq discontinuity, CORRUPTED when there - * is. Each delivery must bump stat_frames_received; only CORRUPTED deliveries - * must additionally bump stat_frames_corrupted. */ -TEST_F(St40PipelineRxTest, FrameStatusCompleteVsCorrupted) { - ASSERT_EQ(stat_frames_received(), 0u); - ASSERT_EQ(stat_frames_corrupted(), 0u); - - /* Clean frame */ - enqueue_burst(0, 4, 1000, true, MTL_SESSION_PORT_P); - process_all(); - - auto* clean = get_frame(); - ASSERT_NE(clean, nullptr); - EXPECT_EQ(clean->status, ST_FRAME_STATUS_COMPLETE); - EXPECT_EQ(stat_frames_received(), 1u); - EXPECT_EQ(stat_frames_corrupted(), 0u); - put_frame(clean); - - /* Corrupted frame: seq gap between 4 and 6. */ - enqueue(4, 2000, false, MTL_SESSION_PORT_P); - enqueue(6, 2000, true, MTL_SESSION_PORT_P); - process_all(); - - auto* corrupted = get_frame(); - ASSERT_NE(corrupted, nullptr); - EXPECT_EQ(corrupted->status, ST_FRAME_STATUS_CORRUPTED); - EXPECT_EQ(stat_frames_received(), 2u); - EXPECT_EQ(stat_frames_corrupted(), 1u); - put_frame(corrupted); - - /* Second corrupted frame: corrupted accumulates independently of received. */ - enqueue(7, 3000, false, MTL_SESSION_PORT_P); - enqueue(9, 3000, true, MTL_SESSION_PORT_P); - process_all(); - - auto* corrupted2 = get_frame(); - ASSERT_NE(corrupted2, nullptr); - EXPECT_EQ(corrupted2->status, ST_FRAME_STATUS_CORRUPTED); - EXPECT_EQ(stat_frames_received(), 3u); - EXPECT_EQ(stat_frames_corrupted(), 2u); - put_frame(corrupted2); - - /* Final clean frame: received bumps, corrupted does not. */ - enqueue_burst(10, 4, 4000, true, MTL_SESSION_PORT_P); - process_all(); - - auto* clean2 = get_frame(); - ASSERT_NE(clean2, nullptr); - EXPECT_EQ(clean2->status, ST_FRAME_STATUS_COMPLETE); - EXPECT_EQ(stat_frames_received(), 4u); - EXPECT_EQ(stat_frames_corrupted(), 2u) << "corrupted must not bump on COMPLETE"; - put_frame(clean2); -} - -/* st40p_rx_get_session_stats() must surface every pipeline-owned frame - * counter exactly as the pipeline tracks it. Drives one clean frame, one - * corrupted frame and a framebuffer-exhaustion drop so all three counters - * are non-zero, then compares the API output to the raw context fields. */ -TEST_F(St40PipelineRxTest, GetSessionStatsOverlay) { - enqueue_burst(0, 4, 1000, true, MTL_SESSION_PORT_P); - process_all(); - auto* f0 = get_frame(); - ASSERT_NE(f0, nullptr); - put_frame(f0); - - enqueue(4, 2000, false, MTL_SESSION_PORT_P); - enqueue(6, 2000, true, MTL_SESSION_PORT_P); /* seq gap */ - process_all(); - auto* f1 = get_frame(); - ASSERT_NE(f1, nullptr); - /* Hold f1 so the next ts changes have no free framebuff and bump dropped. */ - - enqueue(7, 3000, false, MTL_SESSION_PORT_P); - enqueue(8, 4000, false, MTL_SESSION_PORT_P); - enqueue(9, 5000, false, MTL_SESSION_PORT_P); - process_all(); - - struct st40_rx_user_stats api_stats; - ASSERT_EQ(ut40p_get_session_stats(ctx_, &api_stats), 0); - - EXPECT_EQ(api_stats.common.stat_frames_received, stat_frames_received()); - EXPECT_EQ(api_stats.common.stat_frames_dropped, stat_frames_dropped()); - EXPECT_EQ(api_stats.common.stat_frames_corrupted, stat_frames_corrupted()); - /* Guard against a trivially-equal pass on all-zero counters. */ - EXPECT_GT(api_stats.common.stat_frames_received, 0u); - EXPECT_GT(api_stats.common.stat_frames_dropped, 0u); - EXPECT_GT(api_stats.common.stat_frames_corrupted, 0u); - /* Corrupted is a subset of received. */ - EXPECT_LE(api_stats.common.stat_frames_corrupted, - api_stats.common.stat_frames_received); - - put_frame(f1); -} - -/* st40p_rx_reset_session_stats() must zero all three pipeline-owned frame - * counters, and subsequent activity must resume counting from zero. */ -TEST_F(St40PipelineRxTest, ResetClearsFrameCounters) { - /* Make all three counters non-zero. */ - enqueue_burst(0, 4, 1000, true, MTL_SESSION_PORT_P); - process_all(); - auto* f0 = get_frame(); - ASSERT_NE(f0, nullptr); - put_frame(f0); - - enqueue(4, 2000, false, MTL_SESSION_PORT_P); - enqueue(6, 2000, true, MTL_SESSION_PORT_P); - process_all(); - auto* f1 = get_frame(); - ASSERT_NE(f1, nullptr); - - enqueue(7, 3000, false, MTL_SESSION_PORT_P); - enqueue(8, 4000, false, MTL_SESSION_PORT_P); - enqueue(9, 5000, false, MTL_SESSION_PORT_P); - process_all(); - put_frame(f1); - - ASSERT_GT(stat_frames_received(), 0u); - ASSERT_GT(stat_frames_dropped(), 0u); - ASSERT_GT(stat_frames_corrupted(), 0u); - - ASSERT_EQ(ut40p_reset_session_stats(ctx_), 0); - - EXPECT_EQ(stat_frames_received(), 0u); - EXPECT_EQ(stat_frames_dropped(), 0u); - EXPECT_EQ(stat_frames_corrupted(), 0u); - - /* Continue at seq=7 so the first post-reset frame is contiguous with the - * last accepted seq (6) and arrives clean; framebuff-exhaustion drops do - * not advance session_last_seq. */ - enqueue_burst(7, 4, 6000, true, MTL_SESSION_PORT_P); - process_all(); - auto* after = get_frame(); - ASSERT_NE(after, nullptr); - EXPECT_EQ(after->status, ST_FRAME_STATUS_COMPLETE); - EXPECT_EQ(stat_frames_received(), 1u); - EXPECT_EQ(stat_frames_corrupted(), 0u) - << "clean frame after reset must not bump corrupted"; - put_frame(after); -} - -/* ── PENDING state: late-marker resolution ───────────────────────────── */ -/* - * Marker-primary frame assembly with N-1 fallback: - * - * - Marker received → frame immediately READY (zero added latency). - * - Timestamp change without marker → old frame enters PENDING (not READY). - * PENDING frame is not visible to consumer via get_frame(). - * - Late marker for PENDING frame → apply marker, promote to READY. - * - Second timestamp change → force-deliver PENDING frame without marker. - * - At most 2 frames buffered (inflight + pending), bounded memory cost. - */ - -/* When P advances timestamp before R's marker, the old frame enters PENDING. - * It must NOT be visible via get_frame() until resolved. */ -TEST_F(St40PipelineRxTest, PendingFrameNotVisibleBeforeResolution) { - /* Frame body: 4 pkts for ts=1000, no marker */ - enqueue_burst(0, 4, 1000, false, MTL_SESSION_PORT_P); - /* P starts next frame → ts=1000 enters PENDING */ - enqueue(4, 2000, false, MTL_SESSION_PORT_P); - process_all(); - - /* PENDING frame should not be delivered yet */ - EXPECT_EQ(get_frame(), nullptr) - << "PENDING frame must not be visible to consumer before resolution"; - - /* Resolve: send marker for ts=2000 to make it READY, which also - * force-delivers the PENDING ts=1000 frame. */ - enqueue(5, 3000, false, MTL_SESSION_PORT_P); /* second ts change → force-deliver */ - process_all(); - - auto* frame1 = get_frame(); - ASSERT_NE(frame1, nullptr); - EXPECT_EQ(frame1->rtp_timestamp, 1000u); - EXPECT_FALSE(frame1->rtp_marker) << "Force-delivered PENDING frame has no marker"; - put_frame(frame1); -} - -/* Late marker from R resolves a PENDING frame: the frame becomes READY - * with rtp_marker=true and the correct total packet count. */ -TEST_F(St40PipelineRxTest, PendingResolvedByLateMarker) { - /* P: 3 pkts for ts=1000, no marker */ - enqueue_burst(0, 3, 1000, false, MTL_SESSION_PORT_P); - /* P advances to ts=2000 → ts=1000 enters PENDING */ - enqueue(5, 2000, false, MTL_SESSION_PORT_P); - process_all(); - - /* Verify frame not yet visible */ - EXPECT_EQ(get_frame(), nullptr); - - /* R: late packets for ts=1000, marker on last */ - enqueue(3, 1000, false, MTL_SESSION_PORT_R); - enqueue(4, 1000, true, MTL_SESSION_PORT_R); /* MARKER */ - process_all(); - - /* PENDING frame resolved by marker → now READY */ - auto* frame = get_frame(); - ASSERT_NE(frame, nullptr); - EXPECT_EQ(frame->rtp_timestamp, 1000u); - EXPECT_TRUE(frame->rtp_marker) << "Late marker must resolve PENDING frame"; - EXPECT_EQ(frame->pkts_total, 5u) << "Late packets must be counted in frame"; - put_frame(frame); -} - -/* Second timestamp change force-delivers a PENDING frame without marker. - * - * Sequence: ts=1000 (no marker) → ts=2000 (ts=1000 PENDING) → - * ts=3000 (ts=1000 force-READY, ts=2000 PENDING) */ -TEST_F(St40PipelineRxTest, PendingForcedBySecondTimestampChange) { - /* Frame 1: ts=1000, 3 pkts, no marker */ - enqueue_burst(0, 3, 1000, false, MTL_SESSION_PORT_P); - /* ts change → ts=1000 PENDING */ - enqueue(3, 2000, false, MTL_SESSION_PORT_P); - process_all(); - EXPECT_EQ(get_frame(), nullptr) << "ts=1000 should be PENDING, not READY"; - - /* Frame 2: ts=2000, 2 pkts, no marker */ - enqueue(4, 2000, false, MTL_SESSION_PORT_P); - /* Second ts change → ts=1000 force-delivered, ts=2000 PENDING */ - enqueue(5, 3000, false, MTL_SESSION_PORT_P); - process_all(); - - auto* frame1 = get_frame(); - ASSERT_NE(frame1, nullptr); - EXPECT_EQ(frame1->rtp_timestamp, 1000u); - EXPECT_EQ(frame1->pkts_total, 3u); - EXPECT_FALSE(frame1->rtp_marker) << "Force-delivered frame should not have marker"; - put_frame(frame1); - - /* ts=2000 should still be PENDING */ - EXPECT_EQ(get_frame(), nullptr) << "ts=2000 should be PENDING, not yet READY"; -} - -/* Normal marker path: frame with marker goes directly to READY — no PENDING. - * This must continue working unchanged with the PENDING state logic. */ -TEST_F(St40PipelineRxTest, NoPendingWhenMarkerPresent) { - /* Frame with marker on last packet */ - enqueue_burst(0, 4, 1000, true, MTL_SESSION_PORT_P); - process_all(); - - /* Frame should be immediately available — no PENDING state */ - auto* frame = get_frame(); - ASSERT_NE(frame, nullptr); - EXPECT_EQ(frame->rtp_timestamp, 1000u); - EXPECT_TRUE(frame->rtp_marker); - EXPECT_EQ(frame->pkts_total, 4u); - put_frame(frame); - - /* Second frame with marker — also immediate */ - enqueue_burst(4, 3, 2000, true, MTL_SESSION_PORT_P); - process_all(); - - frame = get_frame(); - ASSERT_NE(frame, nullptr); - EXPECT_EQ(frame->rtp_timestamp, 2000u); - EXPECT_TRUE(frame->rtp_marker); - put_frame(frame); -} - -/* A PENDING frame accumulates late packets from R, including their count. - * The marker resolves it and the total reflects all received packets. */ -TEST_F(St40PipelineRxTest, PendingLatePacketsAccumulate) { - /* P: 3 pkts for ts=1000, no marker */ - enqueue_burst(0, 3, 1000, false, MTL_SESSION_PORT_P); - /* P advances to ts=2000 → ts=1000 PENDING */ - enqueue(6, 2000, false, MTL_SESSION_PORT_P); - process_all(); - - /* R: 3 late pkts for ts=1000 (seq 3,4,5), marker on seq 5 */ - enqueue(3, 1000, false, MTL_SESSION_PORT_R); - enqueue(4, 1000, false, MTL_SESSION_PORT_R); - enqueue(5, 1000, true, MTL_SESSION_PORT_R); /* MARKER */ - process_all(); - - auto* frame = get_frame(); - ASSERT_NE(frame, nullptr); - EXPECT_EQ(frame->rtp_timestamp, 1000u); - EXPECT_TRUE(frame->rtp_marker); - EXPECT_EQ(frame->pkts_total, 6u) << "3 from P + 3 from R"; - EXPECT_EQ(frame->pkts_recv[MTL_SESSION_PORT_P], 3u); - EXPECT_EQ(frame->pkts_recv[MTL_SESSION_PORT_R], 3u); - put_frame(frame); -} - -/* Multiple successive frames cycling through PENDING → resolved. - * Each frame's marker arrives late from R after P has advanced. */ -TEST_F(St40PipelineRxTest, PendingMultiFrameCycle) { - /* Frame 1: P sends body, advances to ts=2000 */ - enqueue_burst(0, 3, 1000, false, MTL_SESSION_PORT_P); - enqueue(3, 2000, false, MTL_SESSION_PORT_P); /* ts=1000 → PENDING */ - /* R sends late marker for ts=1000 → resolves PENDING */ - enqueue(3, 1000, true, MTL_SESSION_PORT_R); - - /* Frame 2: P sends body at ts=2000, advances to ts=3000 */ - enqueue(4, 2000, false, MTL_SESSION_PORT_P); - enqueue(5, 3000, false, MTL_SESSION_PORT_P); /* ts=2000 → PENDING */ - /* R sends late marker for ts=2000 */ - enqueue(5, 2000, true, MTL_SESSION_PORT_R); - - /* Frame 3: complete with marker */ - enqueue(6, 3000, true, MTL_SESSION_PORT_P); /* ts=3000 → READY via marker */ - - process_all(); - - auto* f1 = get_frame(); - ASSERT_NE(f1, nullptr); - EXPECT_EQ(f1->rtp_timestamp, 1000u); - EXPECT_TRUE(f1->rtp_marker) << "Frame 1: late marker from R resolved PENDING"; - put_frame(f1); - - auto* f2 = get_frame(); - ASSERT_NE(f2, nullptr); - EXPECT_EQ(f2->rtp_timestamp, 2000u); - EXPECT_TRUE(f2->rtp_marker) << "Frame 2: late marker from R resolved PENDING"; - put_frame(f2); - - auto* f3 = get_frame(); - ASSERT_NE(f3, nullptr); - EXPECT_EQ(f3->rtp_timestamp, 3000u); - EXPECT_TRUE(f3->rtp_marker) << "Frame 3: marker on-time, no PENDING"; - put_frame(f3); - - EXPECT_EQ(get_frame(), nullptr); -} - -/* ── Single-port vs multi-port PENDING behavior ──────────────────────── */ -/* - * These tests verify the behavioral split: single-port sessions never enter - * PENDING (timestamp change → immediate READY), while multi-port sessions - * defer delivery until a late marker or a second timestamp change. - */ - -/* Single-port: timestamp change delivers frame immediately, no PENDING. */ -TEST_F(St40PipelineRxTest, SinglePortTsChangeImmediateReady) { - ut40p_ctx_destroy(ctx_); - ctx_ = ut40p_ctx_create(1, 3); - - enqueue_burst(0, 3, 1000, false, MTL_SESSION_PORT_P); - enqueue(3, 2000, false, MTL_SESSION_PORT_P); /* ts change */ - process_all(); +/* Unlike ST30p, ST40p counts a frame as "received" at transport-delivery + * time (rx_st40p_frame_ready), not when the app calls get_frame. */ +TEST_F(St40PipelineRxTest, FramesReceivedOnFrameReadyNotGetFrame) { + static uint8_t slot0, slot1; - auto* frame = get_frame(); - ASSERT_NE(frame, nullptr) << "Single-port: ts change must deliver frame immediately"; - EXPECT_EQ(frame->rtp_timestamp, 1000u); - EXPECT_FALSE(frame->rtp_marker); - put_frame(frame); -} + ASSERT_EQ(inject(&slot0, 1000), 0); + EXPECT_EQ(frames_received(), 1u) + << "ST40p bumps stat_frames_received at frame_ready, not get_frame"; -/* Multi-port: same packet sequence as above → frame enters PENDING, not visible. */ -TEST_F(St40PipelineRxTest, MultiPortTsChangePending) { - /* Default fixture is 2-port */ - enqueue_burst(0, 3, 1000, false, MTL_SESSION_PORT_P); - enqueue(3, 2000, false, MTL_SESSION_PORT_P); /* ts change */ - process_all(); + ASSERT_EQ(inject(&slot1, 2000), 0); + EXPECT_EQ(frames_received(), 2u); - EXPECT_EQ(get_frame(), nullptr) - << "Multi-port: ts change must enter PENDING, not deliver"; + struct st40_frame_info* f = get_frame(); + ASSERT_NE(f, nullptr); + EXPECT_EQ(frames_received(), 2u) << "get_frame must not bump stat_frames_received"; + EXPECT_EQ(put_frame(f), 0); } -/* Single-port: three rapid ts changes → all three frames immediately READY. - * No PENDING, no force-delivery — each frame completes on ts change. */ -TEST_F(St40PipelineRxTest, SinglePortRapidTsChangesAllReady) { - ut40p_ctx_destroy(ctx_); - ctx_ = ut40p_ctx_create(1, 4); /* 4 fbs to hold all frames */ +/* seq_discont on the delivered meta bumps stat_frames_corrupted at + * frame_ready time; the frame is still delivered (best-effort). */ +TEST_F(St40PipelineRxTest, SeqDiscontFrameBumpsCorruptedStat) { + static uint8_t slot0; - enqueue(0, 1000, false, MTL_SESSION_PORT_P); - enqueue(1, 2000, false, MTL_SESSION_PORT_P); /* completes ts=1000 */ - enqueue(2, 3000, false, MTL_SESSION_PORT_P); /* completes ts=2000 */ - enqueue(3, 4000, false, MTL_SESSION_PORT_P); /* completes ts=3000 */ - process_all(); + ASSERT_EQ(inject_discont(&slot0, 1000), 0); + EXPECT_EQ(frames_received(), 1u) << "corrupted frame is still delivered"; + EXPECT_EQ(frames_corrupted(), 1u); - for (uint32_t ts = 1000; ts <= 3000; ts += 1000) { - auto* f = get_frame(); - ASSERT_NE(f, nullptr) << "ts=" << ts << " must be immediately READY"; - EXPECT_EQ(f->rtp_timestamp, ts); - EXPECT_FALSE(f->rtp_marker); - put_frame(f); - } + struct st40_frame_info* f = get_frame(); + ASSERT_NE(f, nullptr); + EXPECT_EQ(f->status, ST_FRAME_STATUS_CORRUPTED); + EXPECT_EQ(put_frame(f), 0); } -/* Multi-port: three rapid ts changes without markers. - * Frame 1 is force-delivered when frame 2 enters PENDING on the third change. - * Frame 2 stays PENDING. Frame 3 is inflight. */ -TEST_F(St40PipelineRxTest, MultiPortRapidTsChangesForceDelivery) { - ut40p_ctx_destroy(ctx_); - ctx_ = ut40p_ctx_create(2, 4); +/* When all framebufs are full, every further frame_ready bumps + * stat_frames_dropped and stat_busy 1:1, and returns -EBUSY so the + * transport reclaims its slot. */ +TEST_F(St40PipelineRxTest, FramesDroppedWhenFramebufsFull) { + static uint8_t slot0, slot1, slot2, slot3, slot4; - enqueue(0, 1000, false, MTL_SESSION_PORT_P); - enqueue(1, 2000, false, MTL_SESSION_PORT_P); /* ts=1000 → PENDING */ - enqueue(2, 3000, false, MTL_SESSION_PORT_P); /* ts=1000 force-READY, ts=2000 PENDING */ - process_all(); + ASSERT_EQ(inject(&slot0, 1000), 0); + ASSERT_EQ(inject(&slot1, 2000), 0); + ASSERT_EQ(inject(&slot2, 3000), 0); + EXPECT_EQ(frames_dropped(), 0u); + EXPECT_EQ(stat_busy(), 0u); - /* Only ts=1000 should be visible (force-delivered) */ - auto* f1 = get_frame(); - ASSERT_NE(f1, nullptr); - EXPECT_EQ(f1->rtp_timestamp, 1000u); - EXPECT_FALSE(f1->rtp_marker) << "Force-delivered frame has no marker"; - put_frame(f1); + EXPECT_EQ(inject(&slot3, 4000), -EBUSY); + EXPECT_EQ(inject(&slot4, 5000), -EBUSY); - /* ts=2000 is PENDING, ts=3000 is inflight — neither visible */ - EXPECT_EQ(get_frame(), nullptr) << "ts=2000 PENDING, ts=3000 inflight"; + EXPECT_EQ(frames_dropped(), 2u); + EXPECT_EQ(stat_busy(), 2u) << "stat_busy must bump 1:1 with stat_frames_dropped"; + EXPECT_EQ(frames_received(), 3u) << "dropped frames must not count as received"; } -/* Multi-port: PENDING frame occupies a framebuffer slot. - * With 2 fbs total: pending(1) + inflight(1) = 0 free → next ts change hits busy. */ -TEST_F(St40PipelineRxTest, MultiPortPendingReducesAvailableFramebuffers) { - ut40p_ctx_destroy(ctx_); - ctx_ = ut40p_ctx_create(2, 2); +/* put_frame must call st40_rx_put_framebuff() with the frame's transport + * addr exactly once, and clear udw_buff_addr afterward. */ +TEST_F(St40PipelineRxTest, PutFrameReleasesTransportSlotAndClearsAddr) { + static uint8_t slot0; + ut40p_put_framebuff_reset_spy(); - /* ts=1000 fills fb 0 */ - enqueue(0, 1000, false, MTL_SESSION_PORT_P); - /* ts=2000: fb 0 → PENDING, fb 1 starts inflight */ - enqueue(1, 2000, false, MTL_SESSION_PORT_P); - /* ts=3000: fb 0 force-READY, fb 1 → PENDING, try alloc new fb → no free → busy */ - enqueue(2, 3000, false, MTL_SESSION_PORT_P); - process_all(); - - EXPECT_GE(stat_busy(), 1u) - << "PENDING must occupy a framebuffer slot, causing exhaustion"; - - /* Force-delivered ts=1000 should still be retrievable */ - auto* f = get_frame(); + ASSERT_EQ(inject(&slot0, 1000), 0); + struct st40_frame_info* f = get_frame(); ASSERT_NE(f, nullptr); - EXPECT_EQ(f->rtp_timestamp, 1000u); - put_frame(f); -} - -/* Single-port: same 2-fb sequence — no PENDING means both frames are READY. - * The third ts change still hits busy (both fbs occupied), but both are consumable. */ -TEST_F(St40PipelineRxTest, SinglePortNoPendingAvoidsBusyWith2Fbs) { - ut40p_ctx_destroy(ctx_); - ctx_ = ut40p_ctx_create(1, 2); - - enqueue(0, 1000, false, MTL_SESSION_PORT_P); - enqueue(1, 2000, false, MTL_SESSION_PORT_P); /* ts=1000 → READY */ - /* ts=1000 READY (fb 0), ts=2000 inflight (fb 1). No free fb. - * ts=3000 completes ts=2000 → READY (fb 1), tries alloc → fb 0 still READY → busy. */ - enqueue(2, 3000, false, MTL_SESSION_PORT_P); - process_all(); + EXPECT_EQ(f->udw_buff_addr, &slot0); - EXPECT_GE(stat_busy(), 1u) << "ts=3000 hit busy (both fbs occupied)"; - - /* Both frames are READY and consumable — single port doesn't block on PENDING */ - auto* f1 = get_frame(); - ASSERT_NE(f1, nullptr); - EXPECT_EQ(f1->rtp_timestamp, 1000u); - put_frame(f1); - - auto* f2 = get_frame(); - ASSERT_NE(f2, nullptr); - EXPECT_EQ(f2->rtp_timestamp, 2000u); - put_frame(f2); + EXPECT_EQ(put_frame(f), 0); + EXPECT_EQ(ut40p_put_framebuff_call_count(), 1); + EXPECT_EQ(ut40p_put_framebuff_last_addr(), &slot0); + EXPECT_EQ(f->udw_buff_addr, nullptr) + << "put_frame must clear udw_buff_addr after releasing the slot"; } -/* Multi-port: late non-marker packet routes to PENDING but does NOT resolve it. - * The frame stays PENDING until a marker or a second ts change. */ -TEST_F(St40PipelineRxTest, MultiPortLateNonMarkerKeepsPending) { - /* P: body for ts=1000 */ - enqueue_burst(0, 3, 1000, false, MTL_SESSION_PORT_P); - /* P advances → ts=1000 PENDING */ - enqueue(5, 2000, false, MTL_SESSION_PORT_P); - process_all(); +/* put_frame_abort releases the same way but skips the meta_num/stats reset + * that put_frame performs (frame is discarded, not processed). */ +TEST_F(St40PipelineRxTest, PutFrameAbortReleasesTransportSlotAndClearsAddr) { + static uint8_t slot0; + ut40p_put_framebuff_reset_spy(); - /* R: late non-marker packets route to pending ts=1000 */ - enqueue(3, 1000, false, MTL_SESSION_PORT_R); - enqueue(4, 1000, false, MTL_SESSION_PORT_R); /* no marker */ - process_all(); + ASSERT_EQ(inject(&slot0, 1000), 0); + struct st40_frame_info* f = get_frame(); + ASSERT_NE(f, nullptr); - /* Frame must still be invisible (PENDING, not resolved) */ - EXPECT_EQ(get_frame(), nullptr) - << "Non-marker late packets must not resolve PENDING frame"; + EXPECT_EQ(put_frame_abort(f), 0); + EXPECT_EQ(ut40p_put_framebuff_call_count(), 1); + EXPECT_EQ(ut40p_put_framebuff_last_addr(), &slot0); + EXPECT_EQ(f->udw_buff_addr, nullptr); } -/* Multi-port: marker on the current inflight does not affect a pending frame. - * The inflight completes independently; pending stays PENDING. */ -TEST_F(St40PipelineRxTest, MultiPortMarkerOnInflightLeavesPendingAlone) { - /* P: body for ts=1000, no marker */ - enqueue_burst(0, 3, 1000, false, MTL_SESSION_PORT_P); - /* P advances → ts=1000 enters PENDING */ - enqueue(3, 2000, false, MTL_SESSION_PORT_P); - /* P completes ts=2000 with marker */ - enqueue(4, 2000, true, MTL_SESSION_PORT_P); - process_all(); +/* reset_session_stats clears every cumulative pipeline counter. */ +TEST_F(St40PipelineRxTest, ResetClearsAllPipelineCounters) { + static uint8_t slot0, slot1, slot2, slot3, slot4; - /* ts=2000 is READY (marker). ts=1000 is PENDING. - * get_frame scans from consumer_idx: fb 0 (PENDING, skip) → fb 1 (READY). */ - auto* f = get_frame(); + ASSERT_EQ(inject(&slot0, 1000), 0); + struct st40_frame_info* f = get_frame(); ASSERT_NE(f, nullptr); - EXPECT_EQ(f->rtp_timestamp, 2000u) << "Inflight marker delivers independently"; - EXPECT_TRUE(f->rtp_marker); - put_frame(f); + EXPECT_EQ(put_frame(f), 0); + + ASSERT_EQ(inject(&slot1, 2000), 0); + ASSERT_EQ(inject(&slot2, 3000), 0); + ASSERT_EQ(inject(&slot3, 4000), 0); + EXPECT_EQ(inject(&slot4, 5000), -EBUSY); - /* ts=1000 still PENDING — not visible */ - EXPECT_EQ(get_frame(), nullptr) << "PENDING ts=1000 must remain invisible"; + ASSERT_GT(frames_received(), 0u); + ASSERT_GT(frames_dropped(), 0u); - /* Force-deliver ts=1000 via ts change: start ts=3000 inflight, then ts=4000 */ - enqueue(5, 3000, false, MTL_SESSION_PORT_P); /* new inflight, no ts change */ - enqueue(6, 4000, false, - MTL_SESSION_PORT_P); /* ts change: ts=1000 force-READY, ts=3000 PENDING */ - process_all(); + EXPECT_EQ(ut40p_reset_session_stats(ctx_), 0); - auto* f1 = get_frame(); - ASSERT_NE(f1, nullptr); - EXPECT_EQ(f1->rtp_timestamp, 1000u); - EXPECT_FALSE(f1->rtp_marker) << "Force-delivered PENDING frame has no marker"; - put_frame(f1); + EXPECT_EQ(frames_received(), 0u); + EXPECT_EQ(frames_dropped(), 0u); } diff --git a/tests/unit/session/st20/timestamp_source_test.cpp b/tests/unit/session/st20/timestamp_source_test.cpp new file mode 100644 index 000000000..c23846177 --- /dev/null +++ b/tests/unit/session/st20/timestamp_source_test.cpp @@ -0,0 +1,40 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * timestamp_first_pkt sourcing: the RX video path must report the HW + * RX-timestamp dynfield when the interface advertises RX timestamp + * offload, not the software PTP clock. + * + * Build: meson setup build_unit -Denable_unit_tests=true && ninja -C build_unit + * Run: ./build_unit/tests/unit/UnitTest --gtest_filter='St20RxTimestampSourceTest.*' + */ + +#include + +#include "session/st20/st20_rx_test_base.h" + +class St20RxTimestampSourceTest : public St20RxBaseTest { + protected: + int num_port() const override { + return 1; + } +}; + +TEST_F(St20RxTimestampSourceTest, ReceiveTimestampSourcedFromHwOffload) { + constexpr uint64_t kHwRawNs = 987654321000ull; + ut20_ctx_enable_hw_timestamp(ctx_, MTL_SESSION_PORT_P); + + feed_full(1000, MTL_SESSION_PORT_P); + uint64_t sw_only = ut20_last_timestamp_first_pkt(ctx_); + ASSERT_EQ(frames_received(), 1); + EXPECT_NE(sw_only, kHwRawNs) << "sanity: SW ptp stub must not already equal kHwRawNs"; + + for (int i = 0; i < pkts_per_frame(); i++) { + ut20_feed_frame_pkt_hw_ts(ctx_, i, 2000, MTL_SESSION_PORT_P, kHwRawNs); + } + + EXPECT_EQ(frames_received(), 2); + EXPECT_EQ(ut20_last_timestamp_first_pkt(ctx_), kHwRawNs) + << "timestamp_first_pkt must come from the HW RX-timestamp dynfield " + "(mt_mbuf_time_stamp), not the SW PTP clock fallback"; +} diff --git a/tests/unit/session/st20_harness.c b/tests/unit/session/st20_harness.c index 467482be9..f1fb6e0f2 100644 --- a/tests/unit/session/st20_harness.c +++ b/tests/unit/session/st20_harness.c @@ -52,6 +52,8 @@ struct ut20_test_ctx { uint8_t bitmaps[ST_VIDEO_RX_REC_NUM_OFO][UT20_MAX_BITMAP_SIZE]; bool hold_frames; + struct mt_ptp_impl ptp_storage; + uint64_t last_timestamp_first_pkt; }; #include "session/st20_harness.h" @@ -68,9 +70,9 @@ static uint64_t ut20_ptp_time(struct mtl_main_impl* impl, enum mtl_port port) { static int ut20_notify_frame_ready(void* priv, void* frame, struct st20_rx_frame_meta* meta) { - (void)meta; ut20_test_ctx* ctx = priv; if (!ctx || !frame) return 0; + ctx->last_timestamp_first_pkt = meta->timestamp_first_pkt; if (ctx->hold_frames) return 0; struct st_rx_video_session_impl* s = &ctx->session; @@ -278,6 +280,19 @@ int ut20_feed_frame_pkt(ut20_test_ctx* ctx, int pkt_idx, uint32_t ts, return ut20_feed_pkt(ctx, seq, ts, ln, lo, ll, port); } +int ut20_feed_frame_pkt_hw_ts(ut20_test_ctx* ctx, int pkt_idx, uint32_t ts, + enum mtl_session_port port, uint64_t hw_raw_ns) { + uint32_t seq = ts * (uint32_t)ctx->session.ops.height + (uint32_t)pkt_idx; + uint16_t ln, lo, ll; + pkt_idx_to_line(pkt_idx, &ln, &lo, &ll); + struct rte_mbuf* m = make_video_mbuf(seq, ts, ln, lo, ll); + if (!m) return -1; + ut_mbuf_set_hw_timestamp(m, ctx->impl.dynfield_offset, hw_raw_ns); + int rc = rv_handle_frame_pkt(&ctx->session, m, port, true); + rte_pktmbuf_free(m); + return rc; +} + int ut20_feed_frame_pkt_seq(ut20_test_ctx* ctx, int pkt_idx, uint32_t seq, uint32_t ts, enum mtl_session_port port) { uint16_t ln, lo, ll; @@ -386,6 +401,19 @@ void ut20_set_port_down(ut20_test_ctx* ctx, enum mtl_session_port port, bool dow ctx->impl.inf[phy].status &= ~MT_IF_STAT_PORT_DOWN; } +void ut20_ctx_enable_hw_timestamp(ut20_test_ctx* ctx, enum mtl_session_port port) { + enum mtl_port phy = mt_port_logic2phy(ctx->session.port_maps, port); + ctx->impl.dynfield_offset = ut_register_hw_rx_timestamp(); + ctx->ptp_storage.coefficient = 1.0; + ctx->ptp_storage.last_sync_ts = 0; + ctx->impl.ptp[phy] = &ctx->ptp_storage; + ctx->impl.inf[phy].feature |= MT_IF_FEATURE_RX_OFFLOAD_TIMESTAMP; +} + +uint64_t ut20_last_timestamp_first_pkt(const ut20_test_ctx* ctx) { + return ctx->last_timestamp_first_pkt; +} + /* ── stat accessors ───────────────────────────────────────────────────── */ uint64_t ut20_stat_received(const ut20_test_ctx* ctx) { diff --git a/tests/unit/session/st20_harness.h b/tests/unit/session/st20_harness.h index d47f2410f..b1c8e5473 100644 --- a/tests/unit/session/st20_harness.h +++ b/tests/unit/session/st20_harness.h @@ -68,6 +68,12 @@ int ut20_feed_pkt(ut20_test_ctx* ctx, uint32_t seq, uint32_t ts, uint16_t line_n int ut20_feed_frame_pkt(ut20_test_ctx* ctx, int pkt_idx, uint32_t ts, enum mtl_session_port port); +/* Same as ut20_feed_frame_pkt() but stamps the mbuf's HW RX-timestamp + * dynfield with `hw_raw_ns` before feeding it. Requires a prior + * ut20_ctx_enable_hw_timestamp(). */ +int ut20_feed_frame_pkt_hw_ts(ut20_test_ctx* ctx, int pkt_idx, uint32_t ts, + enum mtl_session_port port, uint64_t hw_raw_ns); + /* Same as ut20_feed_frame_pkt() but with an explicit RTP sequence number, * for tests that need to drive seq independently of pkt_idx (e.g. wrap or * threshold tests). */ @@ -109,6 +115,15 @@ void ut20_ctx_set_ssrc(ut20_test_ctx* ctx, uint32_t ssrc); * finalisation (see rv_slot_account_per_port_loss). */ void ut20_set_port_down(ut20_test_ctx* ctx, enum mtl_session_port port, bool down); +/* Enable the HW RX-timestamp offload path on `port`: registers the DPDK + * dynfield, installs an identity-mapped PTP correction, and sets + * MT_IF_FEATURE_RX_OFFLOAD_TIMESTAMP so mt_mbuf_time_stamp() reads the + * mbuf dynfield instead of falling back to the software PTP clock. */ +void ut20_ctx_enable_hw_timestamp(ut20_test_ctx* ctx, enum mtl_session_port port); + +/* timestamp_first_pkt captured off the most recent delivered frame's meta. */ +uint64_t ut20_last_timestamp_first_pkt(const ut20_test_ctx* ctx); + /* Wrapper feeders — drive the production `_handle_mbuf` wrapper instead * of the per-packet handler. Use these (not the direct feeders above) * whenever the test asserts on per-port `err_packets` or per-port diff --git a/tests/unit/session/st40/frame_assembly_test.cpp b/tests/unit/session/st40/frame_assembly_test.cpp new file mode 100644 index 000000000..3ddd2e174 --- /dev/null +++ b/tests/unit/session/st40/frame_assembly_test.cpp @@ -0,0 +1,867 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * FRAME_LEVEL transport assembler: dispatch, frame lifecycle, real ANC + * payload parsing (parity/checksum/stride), and per-frame seq_lost / + * seq_discont derivation from the frame bitmap. + * + * Build: meson setup build_unit -Denable_unit_tests=true && ninja -C build_unit + * Run: ./build_unit/tests/unit/UnitTest + * --gtest_filter='St40Rx*Frame*.*:St40RxBitmapSeqStatsTest.*' + */ + +#include + +#include +#include +#include +#include + +#include "session/st40_harness.h" +#include "st_api.h" + +/* ── FRAME_LEVEL transport assembler dispatch ──────────────────────── */ + +class St40RxFrameLevelDispatchTest : public ::testing::Test { + protected: + ut_test_ctx* ctx_ = nullptr; + + void SetUp() override { + ASSERT_EQ(ut40_init(), 0); + ut40_drain_ring(); + ut40_notify_rtp_calls_reset(); + ctx_ = ut40_ctx_create(2); + ASSERT_NE(ctx_, nullptr); + ut40_set_frame_level(ctx_); /* flip BEFORE first feed */ + } + + void TearDown() override { + ut40_drain_ring(); + ut40_ctx_destroy(ctx_); + ctx_ = nullptr; + } +}; + +/* In FRAME_LEVEL mode the transport must: + * - route every accepted packet to the assembler (stub increments counter) + * - NOT enqueue to the rtp ring + * - NOT call notify_rtp_ready + * - still update common stats and per-port frame counters + */ +TEST_F(St40RxFrameLevelDispatchTest, DispatchesToAssemblerNotRtpRing) { + /* feed a normal frame: 5 packets ending with marker */ + ut40_feed_burst(ctx_, 0, 5, 1000, 1, MTL_SESSION_PORT_P); + + EXPECT_EQ(ut40_stat_assemble_dispatched(ctx_), 5u); + EXPECT_EQ(ut40_stat_received(ctx_), 5u); + EXPECT_EQ(ut40_notify_rtp_calls(), 0) + << "FRAME_LEVEL dispatch must not call notify_rtp_ready"; + EXPECT_EQ(ut40_stat_port_frames(ctx_, MTL_SESSION_PORT_P), 1u); +} + +TEST_F(St40RxFrameLevelDispatchTest, RingStaysEmptyUnderFrameLevel) { + ut40_drain_paused guard; /* don't drain the ring even if RTP path slipped through */ + ut40_feed_burst(ctx_, 0, 4, 2000, 1, MTL_SESSION_PORT_P); + + int count = -1; + bool has_marker = false; + int rc = ut40_ring_dequeue_markers(&count, &has_marker); + EXPECT_EQ(rc, 0); + EXPECT_EQ(count, 0) + << "FRAME_LEVEL dispatch must not enqueue any packet to the rtp ring"; + EXPECT_EQ(ut40_stat_assemble_dispatched(ctx_), 4u); +} + +TEST_F(St40RxFrameLevelDispatchTest, RedundantPacketsStillFiltered) { + /* P then R with same packets — R packets must be classified redundant + * and NOT reach the assembler. */ + ut40_feed_burst(ctx_, 0, 6, 3000, 1, MTL_SESSION_PORT_P); + ut40_feed_burst(ctx_, 0, 6, 3000, 1, MTL_SESSION_PORT_R); + + EXPECT_EQ(ut40_stat_assemble_dispatched(ctx_), 6u) + << "redundant pkts on R must be filtered before assembler dispatch"; + EXPECT_EQ(ut40_stat_redundant(ctx_), 6u); + EXPECT_EQ(ut40_notify_rtp_calls(), 0); +} + +/* ── FRAME_LEVEL transport assembler ────────────────────────────────── */ + +class St40RxFrameAssemblyTest : public ::testing::Test { + protected: + ut_test_ctx* ctx_ = nullptr; + static constexpr uint16_t kSlots = 4; + static constexpr uint32_t kSlotSize = 4096; + + void SetUp() override { + ASSERT_EQ(ut40_init(), 0); + ut40_drain_ring(); + ut40_notify_rtp_calls_reset(); + ut40_captured_reset(); + ctx_ = ut40_ctx_create(2); + ASSERT_NE(ctx_, nullptr); + ut40_setup_frame_pool(ctx_, kSlots, kSlotSize); + } + void TearDown() override { + ut40_teardown_frame_pool(ctx_); + ut40_drain_ring(); + ut40_ctx_destroy(ctx_); + ctx_ = nullptr; + } +}; + +/* Single-port frame: 5 packets, marker on the last, no parsing — assembler + * builds a frame with anc_count=1-per-pkt placeholders and delivers exactly + * once on the marker. */ +TEST_F(St40RxFrameAssemblyTest, SinglePortFrameDeliveredOnMarker) { + constexpr uint32_t ts = 1000; + + for (int i = 0; i < 4; i++) ut40_feed_pkt_anc0(ctx_, i, ts, 0, MTL_SESSION_PORT_P); + EXPECT_EQ(ut40_captured_count(), 0) << "no marker yet → no frame delivered"; + + ut40_feed_pkt_anc0(ctx_, 4, ts, 1, MTL_SESSION_PORT_P); + + ASSERT_EQ(ut40_captured_count(), 1); + EXPECT_EQ(ut40_captured_rtp_ts(0), ts); + EXPECT_TRUE(ut40_captured_marker(0)); + EXPECT_EQ(ut40_captured_meta_num(0), 5) + << "5 pkts → 5 anc meta entries (each pkt anc_count=1)"; + EXPECT_EQ(ut40_stat_anc_frames_ready(ctx_), 1u); + EXPECT_EQ(ut40_stat_anc_frames_dropped(ctx_), 0u); + EXPECT_EQ(ut40_notify_rtp_calls(), 0) << "FRAME_LEVEL must never call notify_rtp_ready"; + EXPECT_NE(ut40_captured_addr(0), nullptr); +} + +/* Single-packet frame: a marker on the very first packet must deliver. */ +TEST_F(St40RxFrameAssemblyTest, SinglePacketFrameWithMarker) { + ut40_feed_pkt_anc0(ctx_, 0, 5000, 1, MTL_SESSION_PORT_P); + ASSERT_EQ(ut40_captured_count(), 1); + EXPECT_EQ(ut40_captured_meta_num(0), 1); + EXPECT_TRUE(ut40_captured_marker(0)); +} + +/* Two consecutive frames: app releases each one, the slot pool recycles. */ +TEST_F(St40RxFrameAssemblyTest, ConsecutiveFramesRecycleSlots) { + for (int i = 0; i < 3; i++) + ut40_feed_pkt_anc0(ctx_, i, 1000, i == 2, MTL_SESSION_PORT_P); + ASSERT_EQ(ut40_captured_count(), 1); + EXPECT_EQ(ut40_stat_anc_frames_ready(ctx_), 1u); + void* a0 = ut40_captured_addr(0); + ASSERT_NE(a0, nullptr); + ut40_release_frame(ctx_, a0); + + for (int i = 0; i < 3; i++) + ut40_feed_pkt_anc0(ctx_, 3 + i, 2000, i == 2, MTL_SESSION_PORT_P); + ASSERT_EQ(ut40_captured_count(), 2); + EXPECT_EQ(ut40_stat_anc_frames_ready(ctx_), 2u); + EXPECT_EQ(ut40_stat_anc_frames_dropped(ctx_), 0u) + << "recycled slot must satisfy alloc — no drops expected"; + EXPECT_EQ(ut40_captured_rtp_ts(1), 2000u); + + /* Hard invariant: with 4 slots and only one in flight at a time, alloc + * MUST recycle the released slot. We don't assert which, but we do assert + * that releasing a0 made it possible at all (no drops). */ + void* a1 = ut40_captured_addr(1); + EXPECT_NE(a1, nullptr); + ut40_release_frame(ctx_, a1); +} + +/* Slot pool exhaustion: 5 frames in flight (no marker) with 4 slots → + * the 5th cannot allocate; stat_anc_frames_dropped++. */ +TEST_F(St40RxFrameAssemblyTest, SlotPoolExhaustionDropsFrame) { + /* Each new tmstamp on a multi-port session pushes the previous inflight to + * PENDING. With kSlots=4 and >4 distinct timestamps in flight, the pool + * eventually exhausts. The intermediate force-deliver-pending path means + * some frames DO get captured along the way — the invariant we assert is + * that at least one frame allocation failed. */ + uint16_t seq = 0; + for (uint32_t ts = 100; ts < 100 + 50 * 8; ts += 50) { + ut40_feed_pkt_anc0(ctx_, seq++, ts, 0, MTL_SESSION_PORT_P); + } + + EXPECT_GE(ut40_stat_anc_frames_dropped(ctx_), 1u); +} + +/* notify_frame_ready returns -1 → the assembler must reclaim the slot + * (state → FREE) so the pool stays usable for the next frame. */ +TEST_F(St40RxFrameAssemblyTest, NotifyFailureReclaimsSlot) { + ut40_set_notify_frame_fail_after(0); /* fail the very first call */ + for (int i = 0; i < 3; i++) + ut40_feed_pkt_anc0(ctx_, i, 1000, i == 2, MTL_SESSION_PORT_P); + EXPECT_EQ(ut40_captured_count(), 0); + /* The failed delivery still counts toward stat_anc_frames_ready (assembly + * succeeded; only the app rejected it). What matters: pool is reusable. */ + EXPECT_EQ(ut40_stat_anc_frames_ready(ctx_), 1u); + + /* Feed enough independent frames to exhaust the pool 1+ times over. + * If reclamation worked, all should land. If not, at least one drops. */ + ut40_set_notify_frame_fail_after(-1); + for (uint16_t i = 0; i < kSlots * 2; i++) { + uint32_t ts = 2000u + 1000u * i; + /* single-pkt frames so each completes immediately and we reuse slot */ + ut40_feed_pkt_anc0(ctx_, 10 + i, ts, 1, MTL_SESSION_PORT_P); + void* addr = ut40_captured_addr(ut40_captured_count() - 1); + if (addr) ut40_release_frame(ctx_, addr); + } + EXPECT_EQ(ut40_stat_anc_frames_dropped(ctx_), 0u) + << "failed delivery must not leak the slot — pool stays usable"; + EXPECT_EQ(ut40_captured_count(), (int)(kSlots * 2)); +} + +/* Cross-port redundancy: P sends pkts 0..4 (loses marker), ts moves on + * (ts=2000) on P → inflight rolls to PENDING. Then R supplies the missed + * marker pkt 5 with ts=1000 → assembler delivers the pending frame with + * its marker. */ +TEST_F(St40RxFrameAssemblyTest, PendingFrameResolvedByLateMarker) { + /* P frame ts=1000 missing marker (would be at seq 5) */ + for (int i = 0; i < 5; i++) ut40_feed_pkt_anc0(ctx_, i, 1000, 0, MTL_SESSION_PORT_P); + EXPECT_EQ(ut40_captured_count(), 0); + + /* P starts next frame ts=2000 with seq 6 → triggers PENDING transition. */ + ut40_feed_pkt_anc0(ctx_, 6, 2000, 0, MTL_SESSION_PORT_P); + EXPECT_EQ(ut40_captured_count(), 0) + << "ts change in multi-port mode must NOT deliver yet (PENDING)"; + + /* R supplies the missing marker pkt for ts=1000 using unique seq=5 + * (P never sent it). Redundancy filter accepts it; assembler matches + * tmstamp to the PENDING slot. */ + ut40_feed_pkt_anc0(ctx_, 5, 1000, 1, MTL_SESSION_PORT_R); + + ASSERT_EQ(ut40_captured_count(), 1); + EXPECT_EQ(ut40_captured_rtp_ts(0), 1000u) << "PENDING frame is the one delivered"; + EXPECT_TRUE(ut40_captured_marker(0)); +} + +/* A PENDING frame accumulates late non-marker packets from R without being + * prematurely resolved; only the late marker delivers it, with the combined + * packet count from both ports. */ +TEST_F(St40RxFrameAssemblyTest, PendingLatePacketsAccumulate) { + /* P: 3 pkts for ts=1000, no marker */ + for (int i = 0; i < 3; i++) ut40_feed_pkt_anc0(ctx_, i, 1000, 0, MTL_SESSION_PORT_P); + /* P advances to ts=2000 → ts=1000 rolls to PENDING */ + ut40_feed_pkt_anc0(ctx_, 6, 2000, 0, MTL_SESSION_PORT_P); + EXPECT_EQ(ut40_captured_count(), 0); + + /* R: 3 late pkts for ts=1000 (seq 3,4,5); marker only on the last. */ + ut40_feed_pkt_anc0(ctx_, 3, 1000, 0, MTL_SESSION_PORT_R); + EXPECT_EQ(ut40_captured_count(), 0) + << "late non-marker packet must not resolve the PENDING frame"; + ut40_feed_pkt_anc0(ctx_, 4, 1000, 0, MTL_SESSION_PORT_R); + EXPECT_EQ(ut40_captured_count(), 0) + << "late non-marker packet must not resolve the PENDING frame"; + ut40_feed_pkt_anc0(ctx_, 5, 1000, 1, MTL_SESSION_PORT_R); + + ASSERT_EQ(ut40_captured_count(), 1); + EXPECT_EQ(ut40_captured_rtp_ts(0), 1000u); + EXPECT_TRUE(ut40_captured_marker(0)); + EXPECT_EQ(ut40_captured_pkts_total(0), 6u) << "3 from P + 3 from R"; + EXPECT_EQ(ut40_captured_port_pkts_recv(0, MTL_SESSION_PORT_P), 3u); + EXPECT_EQ(ut40_captured_port_pkts_recv(0, MTL_SESSION_PORT_R), 3u); +} + +/* A marker on the current inflight frame delivers it independently and must + * not touch an already-PENDING frame, which stays invisible until its own + * marker or a further timestamp change force-delivers it. */ +TEST_F(St40RxFrameAssemblyTest, MultiPortMarkerOnInflightLeavesPendingAlone) { + /* P: body for ts=1000, no marker */ + for (int i = 0; i < 3; i++) ut40_feed_pkt_anc0(ctx_, i, 1000, 0, MTL_SESSION_PORT_P); + /* P advances → ts=1000 enters PENDING */ + ut40_feed_pkt_anc0(ctx_, 3, 2000, 0, MTL_SESSION_PORT_P); + /* P completes ts=2000 with marker */ + ut40_feed_pkt_anc0(ctx_, 4, 2000, 1, MTL_SESSION_PORT_P); + + ASSERT_EQ(ut40_captured_count(), 1) << "inflight marker delivers independently"; + EXPECT_EQ(ut40_captured_rtp_ts(0), 2000u); + EXPECT_TRUE(ut40_captured_marker(0)); + + /* ts=1000 is still PENDING — force-deliver it via two more ts changes: + * start ts=3000 inflight, then ts=4000 forces ts=1000 out. */ + ut40_feed_pkt_anc0(ctx_, 5, 3000, 0, MTL_SESSION_PORT_P); + ut40_feed_pkt_anc0(ctx_, 6, 4000, 0, MTL_SESSION_PORT_P); + + ASSERT_EQ(ut40_captured_count(), 2); + EXPECT_EQ(ut40_captured_rtp_ts(1), 1000u); + EXPECT_FALSE(ut40_captured_marker(1)) << "force-delivered PENDING frame has no marker"; +} + +/* Single-port: timestamp change without prior marker forces immediate + * delivery of the inflight frame (no PENDING dance because num_port==1). */ +TEST_F(St40RxFrameAssemblyTest, SinglePortTimestampChangeForcesDelivery) { + ut40_teardown_frame_pool(ctx_); + ut40_ctx_destroy(ctx_); + ctx_ = ut40_ctx_create(1); /* single port */ + ASSERT_NE(ctx_, nullptr); + ut40_setup_frame_pool(ctx_, kSlots, kSlotSize); + ut40_captured_reset(); + + for (int i = 0; i < 3; i++) ut40_feed_pkt_anc0(ctx_, i, 1000, 0, MTL_SESSION_PORT_P); + EXPECT_EQ(ut40_captured_count(), 0); + + /* ts change → single-port path delivers immediately, even without marker. */ + ut40_feed_pkt_anc0(ctx_, 3, 2000, 0, MTL_SESSION_PORT_P); + ASSERT_EQ(ut40_captured_count(), 1); + EXPECT_EQ(ut40_captured_rtp_ts(0), 1000u); + EXPECT_FALSE(ut40_captured_marker(0)) << "no marker arrived for the first frame"; +} + +/* RealAncPayload: also validate sdid round-trip + that meta_offset chains. */ +TEST_F(St40RxFrameAssemblyTest, RealAncPayloadDecodedIntoMetaAndBuf) { + uint8_t udw[8] = {0xA1, 0xB2, 0xC3, 0xD4, 0xE5, 0xF6, 0x07, 0x18}; + int rc = ut40_feed_anc_pkt(ctx_, 0, 1000, 1, MTL_SESSION_PORT_P, + /*did*/ 0x41, /*sdid*/ 0x05, udw, sizeof(udw), + /*corrupt_parity*/ -1, /*corrupt_cs*/ false); + ASSERT_EQ(rc, 0); + ASSERT_EQ(ut40_captured_count(), 1); + EXPECT_EQ(ut40_captured_meta_num(0), 1); + EXPECT_EQ(ut40_captured_meta_did(0, 0), 0x41); + EXPECT_EQ(ut40_captured_meta_sdid(0, 0), 0x05); + EXPECT_EQ(ut40_captured_meta_udw_size(0, 0), (int)sizeof(udw)); + EXPECT_EQ(ut40_captured_meta_udw_offset(0, 0), 0u) + << "first meta entry must start at udw offset 0"; + EXPECT_EQ(ut40_captured_udw_fill(0), sizeof(udw)); + for (uint32_t i = 0; i < sizeof(udw); i++) { + EXPECT_EQ(ut40_captured_udw_byte(0, i), udw[i]) << "udw byte " << i << " mismatch"; + } + EXPECT_EQ(ut40_stat_anc_pkt_parse_err(ctx_), 0u); +} + +/* Two separate RTP packets (two rx_anc_slot_parse_pkt() calls) contributing + * to the same frame: the second packet's meta_entry->udw_offset must reflect + * the running slot->udw_buffer_fill left by the first packet, not 0. Each + * decode_packet() call writes udw_offset=0 into its own meta struct (it has + * no notion of frame-relative offset) — the caller must overwrite it with + * the pre-call fill *after* the decode call, since decode_packet() clobbers + * whatever was set before it. */ +TEST_F(St40RxFrameAssemblyTest, CrossPacketUdwOffsetThreadsAcrossRtpPackets) { + uint8_t udw0[4] = {0x11, 0x22, 0x33, 0x44}; + uint8_t udw1[6] = {0x55, 0x66, 0x77, 0x88, 0x99, 0xAA}; + + ASSERT_EQ(ut40_feed_anc_pkt(ctx_, 0, 1000, 0, MTL_SESSION_PORT_P, 0x41, 0x05, udw0, + sizeof(udw0), -1, false), + 0); + ASSERT_EQ(ut40_feed_anc_pkt(ctx_, 1, 1000, 1, MTL_SESSION_PORT_P, 0x41, 0x05, udw1, + sizeof(udw1), -1, false), + 0); + + ASSERT_EQ(ut40_captured_count(), 1); + ASSERT_EQ(ut40_captured_meta_num(0), 2); + EXPECT_EQ(ut40_captured_meta_udw_offset(0, 0), 0u); + EXPECT_EQ(ut40_captured_meta_udw_size(0, 0), (int)sizeof(udw0)); + EXPECT_EQ(ut40_captured_meta_udw_offset(0, 1), sizeof(udw0)) + << "second RTP packet's meta entry must start where the first left off"; + EXPECT_EQ(ut40_captured_meta_udw_size(0, 1), (int)sizeof(udw1)); + EXPECT_EQ(ut40_captured_udw_fill(0), sizeof(udw0) + sizeof(udw1)); + for (uint32_t i = 0; i < sizeof(udw1); i++) + EXPECT_EQ(ut40_captured_udw_byte(0, sizeof(udw0) + i), udw1[i]); + EXPECT_EQ(ut40_stat_anc_pkt_parse_err(ctx_), 0u); +} + +/* Multiple ANC data packets carried in a single RTP packet (non-split mode). + * Validates that rx_anc_slot_parse_pkt() correctly walks the variable-length + * per-ANC payload stride when the 10-bit UDW packing of one or more blocks + * is not byte-aligned, and recovers the metadata and user data words for + * every ANC packet in the frame. + * + * Two size patterns are exercised because a wrong (floor-rounded) stride + * manifests differently depending on what the misread bytes decode to: + * {8, 6, 4} — the misread data_count lands on zero padding, so the + * parser silently produces a short/empty meta entry. + * {6, 6, 6} — the misread data_count decodes to a large value that + * overflows the remaining payload room, surfacing as a + * parse error and meta_num short of anc_count. */ +TEST_F(St40RxFrameAssemblyTest, MultiAncInSingleRtpPacketByteStride) { + const uint16_t patterns[][3] = {{8, 6, 4}, {6, 6, 6}}; + const uint8_t anc_count = 3; + uint32_t ts = 1000; + uint16_t seq = 0; + + for (const auto& udw_sizes : patterns) { + ut40_captured_reset(); + int rc = ut40_feed_multi_anc_pkt(ctx_, seq++, ts, 1, MTL_SESSION_PORT_P, udw_sizes, + anc_count); + ASSERT_EQ(rc, 0); + + ASSERT_EQ(ut40_captured_count(), 1); + EXPECT_EQ(ut40_captured_rtp_ts(0), ts); + EXPECT_TRUE(ut40_captured_marker(0)); + ASSERT_EQ(ut40_captured_meta_num(0), anc_count); + + uint32_t expected_offset = 0; + for (uint8_t anc_idx = 0; anc_idx < anc_count; anc_idx++) { + EXPECT_EQ(ut40_captured_meta_did(0, anc_idx), 0x41); + EXPECT_EQ(ut40_captured_meta_sdid(0, anc_idx), 0x05); + EXPECT_EQ(ut40_captured_meta_udw_size(0, anc_idx), udw_sizes[anc_idx]); + EXPECT_EQ(ut40_captured_meta_udw_offset(0, anc_idx), expected_offset); + + for (uint16_t udw_idx = 0; udw_idx < udw_sizes[anc_idx]; udw_idx++) { + uint8_t expected = (uint8_t)(((anc_idx + 1) * 17 + udw_idx) & 0xff); + EXPECT_EQ(ut40_captured_udw_byte(0, expected_offset + udw_idx), expected); + } + expected_offset += udw_sizes[anc_idx]; + } + EXPECT_EQ(ut40_captured_udw_fill(0), expected_offset); + EXPECT_EQ(ut40_stat_anc_pkt_parse_err(ctx_), 0u); + + ut40_release_frame(ctx_, ut40_captured_addr(0)); + ts += 1000; + } +} + +/* UDW parity failure: parser must abort the ANC chunk, NOT increment + * meta_num, AND record stat_anc_pkt_parse_err. The frame is still + * delivered on marker (best-effort) but with meta_num=0. */ +TEST_F(St40RxFrameAssemblyTest, UdwParityFailureRecordedAndChunkSkipped) { + uint8_t udw[4] = {0x11, 0x22, 0x33, 0x44}; + int rc = ut40_feed_anc_pkt(ctx_, 0, 1000, 1, MTL_SESSION_PORT_P, 0x41, 0x05, udw, + sizeof(udw), /*corrupt_parity*/ 2, false); + ASSERT_EQ(rc, 0); + ASSERT_EQ(ut40_captured_count(), 1); + EXPECT_EQ(ut40_captured_meta_num(0), 0) << "parity failure must NOT add a meta entry"; + EXPECT_EQ(ut40_captured_udw_fill(0), 0u) + << "udw_buffer_fill must roll back to original on parity error"; + EXPECT_EQ(ut40_stat_anc_pkt_parse_err(ctx_), 1u) + << "exactly one parse error must be recorded"; +} + +/* Checksum failure: parser must abort, no meta entry added. */ +TEST_F(St40RxFrameAssemblyTest, ChecksumFailureRecorded) { + uint8_t udw[4] = {0x11, 0x22, 0x33, 0x44}; + int rc = ut40_feed_anc_pkt(ctx_, 0, 1000, 1, MTL_SESSION_PORT_P, 0x41, 0x05, udw, + sizeof(udw), /*corrupt_parity*/ -1, + /*corrupt_cs*/ true); + ASSERT_EQ(rc, 0); + ASSERT_EQ(ut40_captured_count(), 1); + EXPECT_EQ(ut40_captured_meta_num(0), 0); + EXPECT_EQ(ut40_captured_udw_fill(0), 0u) + << "checksum failure must roll back udw_buffer_fill"; + EXPECT_EQ(ut40_stat_anc_pkt_parse_err(ctx_), 1u); +} + +/* Empty ANC packet (udw_size=0): valid per spec, must be preserved as a + * meta entry with udw_size=0. */ +TEST_F(St40RxFrameAssemblyTest, EmptyAncPacketAccepted) { + int rc = ut40_feed_anc_pkt(ctx_, 0, 1000, 1, MTL_SESSION_PORT_P, 0x41, 0x05, nullptr, 0, + -1, false); + ASSERT_EQ(rc, 0); + ASSERT_EQ(ut40_captured_count(), 1); + EXPECT_EQ(ut40_captured_meta_num(0), 1); + EXPECT_EQ(ut40_captured_meta_udw_size(0, 0), 0); + EXPECT_EQ(ut40_captured_udw_fill(0), 0u); + EXPECT_EQ(ut40_stat_anc_pkt_parse_err(ctx_), 0u); +} + +/* RFC 8331 requires the checksum to be validated even when udw_size==0; a + * corrupted checksum on an otherwise-empty ANC sub-packet must be rejected, + * not silently accepted. */ +TEST_F(St40RxFrameAssemblyTest, ChecksumFailureRecordedForEmptyAncPacket) { + int rc = ut40_feed_anc_pkt(ctx_, 0, 1000, 1, MTL_SESSION_PORT_P, 0x41, 0x05, nullptr, 0, + -1, /*corrupt_cs*/ true); + ASSERT_EQ(rc, 0); + ASSERT_EQ(ut40_captured_count(), 1); + EXPECT_EQ(ut40_captured_meta_num(0), 0); + EXPECT_EQ(ut40_captured_udw_fill(0), 0u); + EXPECT_EQ(ut40_stat_anc_pkt_parse_err(ctx_), 1u); +} + +/* UDW buffer overflow: feeding many large ANC packets eventually overflows + * the per-frame udw_buf. The assembler must record parse_err and not + * write past the buffer. */ +TEST_F(St40RxFrameAssemblyTest, UdwBufferOverflowProtected) { + /* Tiny pool size so we overflow quickly. */ + ut40_teardown_frame_pool(ctx_); + ut40_setup_frame_pool(ctx_, /*cnt*/ 2, /*size*/ 32); + + uint8_t udw[20]; + memset(udw, 0xAA, sizeof(udw)); + + /* Pkt 1: 20 bytes → fits. fill==20. parse_err==0. */ + ut40_feed_anc_pkt(ctx_, 0, 1000, 0, MTL_SESSION_PORT_P, 0x41, 0x05, udw, sizeof(udw), + -1, false); + /* Pkt 2: would need 20 more, only 12 free → overflow at byte 12. + * Rollback to original_fill=20. parse_err==1. */ + ut40_feed_anc_pkt(ctx_, 1, 1000, 0, MTL_SESSION_PORT_P, 0x41, 0x05, udw, sizeof(udw), + -1, false); + /* Pkt 3: same overflow. parse_err==2. fill stays 20. Marker delivers. */ + ut40_feed_anc_pkt(ctx_, 2, 1000, 1, MTL_SESSION_PORT_P, 0x41, 0x05, udw, sizeof(udw), + -1, false); + + ASSERT_EQ(ut40_captured_count(), 1); + EXPECT_EQ(ut40_captured_meta_num(0), 1) + << "only the first ANC chunk should have been accepted"; + EXPECT_EQ(ut40_captured_udw_fill(0), 20u) + << "fill must equal exactly the bytes accepted from pkt 1"; + EXPECT_EQ(ut40_stat_anc_pkt_parse_err(ctx_), 2u) + << "two overflow rejections expected (pkt 2 + pkt 3)"; + /* No buffer overrun: read all 32 udw bytes — first 20 must be 0xAA, rest 0. */ + for (uint32_t i = 0; i < 20; i++) EXPECT_EQ(ut40_captured_udw_byte(0, i), 0xAAu); +} + +/* Multi-port force-deliver: in a 2-port session, two consecutive timestamps + * with NO marker → first frame is force-delivered when the THIRD timestamp + * arrives (the previous PENDING is flushed before a new one takes its slot). */ +TEST_F(St40RxFrameAssemblyTest, MultiPortForceDeliversStalePending) { + /* ts=1000 no marker → inflight */ + ut40_feed_pkt_anc0(ctx_, 0, 1000, 0, MTL_SESSION_PORT_P); + EXPECT_EQ(ut40_captured_count(), 0); + /* ts=2000 no marker → inflight (1000) rolls to PENDING. Nothing delivered. */ + ut40_feed_pkt_anc0(ctx_, 1, 2000, 0, MTL_SESSION_PORT_P); + EXPECT_EQ(ut40_captured_count(), 0); + /* ts=3000 → existing PENDING (1000) is force-delivered, new inflight + * for 3000, previous inflight (2000) becomes PENDING. */ + ut40_feed_pkt_anc0(ctx_, 2, 3000, 0, MTL_SESSION_PORT_P); + ASSERT_EQ(ut40_captured_count(), 1); + EXPECT_EQ(ut40_captured_rtp_ts(0), 1000u) + << "the OLDEST pending frame is the one force-delivered"; + EXPECT_FALSE(ut40_captured_marker(0)) << "force-delivered without marker"; + EXPECT_EQ(ut40_stat_anc_frames_ready(ctx_), 1u); +} + +/* Pool exhaustion right after inflight is cleared by a plain marker delivery + * (not an eviction rollover) must not strand a stale PENDING frame forever — + * it must be force-delivered on the next mismatched timestamp even though + * anc_inflight_slot is NULL, not only when a rollover happens to touch it. */ +TEST_F(St40RxFrameAssemblyTest, PoolExhaustionForceDeliversPendingWithNoInflight) { + ut40_teardown_frame_pool(ctx_); + ut40_ctx_destroy(ctx_); + ctx_ = ut40_ctx_create(2); /* multi-port: PENDING state is reachable */ + ASSERT_NE(ctx_, nullptr); + ut40_setup_frame_pool(ctx_, /*cnt*/ 2, /*size*/ 256); + ut40_captured_reset(); + + /* ts=1000 no marker → inflight (1 of 2 slots used). */ + ut40_feed_pkt_anc0(ctx_, 0, 1000, 0, MTL_SESSION_PORT_P); + /* ts=2000 no marker → ts=1000 rolls to PENDING, ts=2000 takes the last + * free slot as inflight (2 of 2 used). */ + ut40_feed_pkt_anc0(ctx_, 1, 2000, 0, MTL_SESSION_PORT_P); + EXPECT_EQ(ut40_captured_count(), 0); + /* ts=2000 completes with a marker: inflight clears to NULL directly + * (no rollover), delivered slot stays IN_USER — pool still fully occupied. */ + ut40_feed_pkt_anc0(ctx_, 2, 2000, 1, MTL_SESSION_PORT_P); + ASSERT_EQ(ut40_captured_count(), 1); + EXPECT_EQ(ut40_captured_rtp_ts(0), 2000u); + + /* ts=3000: no free slot, and no inflight to roll over — the stale + * ts=1000 PENDING must still be force-delivered instead of stalling. */ + ut40_feed_pkt_anc0(ctx_, 3, 3000, 0, MTL_SESSION_PORT_P); + ASSERT_EQ(ut40_captured_count(), 2) + << "stale PENDING must be force-delivered even with no inflight slot"; + EXPECT_EQ(ut40_captured_rtp_ts(1), 1000u); + EXPECT_FALSE(ut40_captured_marker(1)); + EXPECT_GE(ut40_stat_anc_frames_dropped(ctx_), 1u) + << "ts=3000 itself still has nowhere to go — pool remains exhausted"; +} + +/* A pending slot created by *this same call's* inflight rollover must not be + * immediately force-delivered by the pool-exhaustion fallback below it — that + * would give it zero chance to receive its own late marker. Only a pending + * slot that already existed before this call (a genuinely stale one) may be + * force-delivered on exhaustion. */ +TEST_F(St40RxFrameAssemblyTest, PoolExhaustionDoesNotForceDeliverPendingCreatedThisCall) { + ut40_teardown_frame_pool(ctx_); + ut40_ctx_destroy(ctx_); + ctx_ = ut40_ctx_create(2); /* multi-port: PENDING state is reachable */ + ASSERT_NE(ctx_, nullptr); + ut40_setup_frame_pool(ctx_, /*cnt*/ 2, /*size*/ 256); + ut40_captured_reset(); + + /* ts=500 completes with a marker → IN_USER, occupies 1 of 2 slots (app + * never releases it, so it stays occupied for the rest of this test). */ + ut40_feed_pkt_anc0(ctx_, 0, 500, 1, MTL_SESSION_PORT_P); + ASSERT_EQ(ut40_captured_count(), 1); + + /* ts=1000 no marker → takes the last free slot as inflight (2 of 2 used). */ + ut40_feed_pkt_anc0(ctx_, 1, 1000, 0, MTL_SESSION_PORT_P); + EXPECT_EQ(ut40_captured_count(), 1); + + /* ts=2000: pool is already exhausted, so this one call both rolls ts=1000 + * to PENDING *and* hits the fresh-alloc fallback. ts=1000 must NOT be + * force-delivered here — it was just parked this same call. */ + ut40_feed_pkt_anc0(ctx_, 2, 2000, 0, MTL_SESSION_PORT_P); + EXPECT_EQ(ut40_captured_count(), 1) + << "a pending slot created by this same call's rollover must not be " + "force-delivered without a late-marker grace period"; + EXPECT_GE(ut40_stat_anc_frames_dropped(ctx_), 1u) + << "ts=2000 itself has nowhere to go since both slots are occupied"; +} + +/* Cross-port redundancy: identical packets on R after P must be filtered + * and never reach the assembler. The frame is delivered exactly once. */ +TEST_F(St40RxFrameAssemblyTest, RedundantCopyDoesNotDuplicateFrame) { + for (int i = 0; i < 4; i++) + ut40_feed_pkt_anc0(ctx_, i, 1000, i == 3, MTL_SESSION_PORT_P); + ASSERT_EQ(ut40_captured_count(), 1); + EXPECT_EQ(ut40_stat_anc_frames_ready(ctx_), 1u); + + /* R replays the same frame — must be 100% absorbed by redundancy filter. */ + uint64_t ready_before = ut40_stat_anc_frames_ready(ctx_); + uint64_t dispatched_before = ut40_stat_assemble_dispatched(ctx_); + for (int i = 0; i < 4; i++) + ut40_feed_pkt_anc0(ctx_, i, 1000, i == 3, MTL_SESSION_PORT_R); + + EXPECT_EQ(ut40_captured_count(), 1) + << "redundant copy must not produce a second delivery"; + EXPECT_EQ(ut40_stat_anc_frames_ready(ctx_), ready_before); + EXPECT_EQ(ut40_stat_assemble_dispatched(ctx_), dispatched_before) + << "redundancy filter must reject before dispatch \u2014 not 1 dispatch slipped"; +} + +/* Slot pool size = 1: only one in-flight frame allowed. A second frame in a + * single-port session that sees an inflight, ts changes, must drop because + * the inflight already occupied the only slot AND deliver it. */ +TEST_F(St40RxFrameAssemblyTest, SinglePortSingleSlotPoolBoundary) { + ut40_teardown_frame_pool(ctx_); + ut40_ctx_destroy(ctx_); + ctx_ = ut40_ctx_create(1); + ASSERT_NE(ctx_, nullptr); + ut40_setup_frame_pool(ctx_, /*cnt*/ 1, /*size*/ 256); + ut40_captured_reset(); + + /* First frame ts=1000: no marker. */ + ut40_feed_pkt_anc0(ctx_, 0, 1000, 0, MTL_SESSION_PORT_P); + EXPECT_EQ(ut40_captured_count(), 0); + /* ts=2000 → single-port path force-delivers ts=1000 (slot freed by IN_USER + * notify), then needs a fresh slot for ts=2000. App hasn't released yet, + * so the slot is IN_USER and not FREE → drop. */ + ut40_feed_pkt_anc0(ctx_, 1, 2000, 1, MTL_SESSION_PORT_P); + EXPECT_EQ(ut40_captured_count(), 1) << "first frame force-delivered"; + EXPECT_EQ(ut40_captured_rtp_ts(0), 1000u); + EXPECT_GE(ut40_stat_anc_frames_dropped(ctx_), 1u) + << "second frame had nowhere to go (only slot held by app)"; + + /* Release and verify pool is reusable. */ + ut40_release_frame(ctx_, ut40_captured_addr(0)); + ut40_feed_pkt_anc0(ctx_, 2, 3000, 1, MTL_SESSION_PORT_P); + EXPECT_EQ(ut40_captured_count(), 2) << "after release, next frame must land"; +} + +/* receive_timestamp must be a real, non-zero, monotonically increasing + * capture time (timestamp_first_pkt in the delivered meta), not the + * hardcoded 0 the assembler used before wiring the PTP clock. */ +TEST_F(St40RxFrameAssemblyTest, ReceiveTimestampIsNonZeroAndMonotonic) { + ut40_feed_pkt_anc0(ctx_, 0, 1000, 1, MTL_SESSION_PORT_P); + ut40_feed_pkt_anc0(ctx_, 1, 2000, 1, MTL_SESSION_PORT_P); + ut40_feed_pkt_anc0(ctx_, 2, 3000, 1, MTL_SESSION_PORT_P); + + ASSERT_EQ(ut40_captured_count(), 3); + EXPECT_GT(ut40_captured_timestamp_first_pkt(0), 0u); + EXPECT_GT(ut40_captured_timestamp_first_pkt(1), 0u); + EXPECT_GT(ut40_captured_timestamp_first_pkt(2), 0u); + EXPECT_LT(ut40_captured_timestamp_first_pkt(0), ut40_captured_timestamp_first_pkt(1)); + EXPECT_LT(ut40_captured_timestamp_first_pkt(1), ut40_captured_timestamp_first_pkt(2)); +} + +/* timestamp_first_pkt must come from the HW RX-timestamp dynfield + * (mt_mbuf_time_stamp) when the interface advertises RX timestamp offload, + * not the software PTP clock fallback. */ +TEST_F(St40RxFrameAssemblyTest, ReceiveTimestampSourcedFromHwOffload) { + constexpr uint64_t kHwRawNs = 123456789000ull; + ut40_ctx_enable_hw_timestamp(ctx_, MTL_SESSION_PORT_P); + + ut40_feed_pkt_anc0_hw_ts(ctx_, 0, 1000, 1, MTL_SESSION_PORT_P, kHwRawNs); + + ASSERT_EQ(ut40_captured_count(), 1); + EXPECT_EQ(ut40_captured_timestamp_first_pkt(0), kHwRawNs) + << "timestamp_first_pkt must come from the HW RX-timestamp dynfield " + "(mt_mbuf_time_stamp), not the SW PTP clock fallback"; +} + +/* Regression guard for the unsynchronized frame_slots[].state field: the app + * thread (st40_rx_put_framebuff) frees slots concurrently with the tasklet + * thread scanning/assembling. Every fed frame must end up counted exactly + * once, as either delivered or dropped, never both and never neither. */ +TEST_F(St40RxFrameAssemblyTest, ConcurrentPutFramebuffDoesNotRaceWithSlotScan) { + ut40_teardown_frame_pool(ctx_); + ut40_ctx_destroy(ctx_); + ctx_ = ut40_ctx_create(1); /* single port: every marker resolves immediately */ + ASSERT_NE(ctx_, nullptr); + ut40_setup_frame_pool(ctx_, kSlots, kSlotSize); + ut40_captured_reset(); + + constexpr int kWaves = 8; + constexpr int kPerWave = 20; /* stays within the harness capture buffer */ + std::mutex mu; + std::queue ready_addrs; + std::atomic stop{false}; + + std::thread releaser([&] { + while (!stop.load(std::memory_order_relaxed)) { + void* addr = nullptr; + { + std::lock_guard lock(mu); + if (!ready_addrs.empty()) { + addr = ready_addrs.front(); + ready_addrs.pop(); + } + } + if (addr) ut40_release_frame(ctx_, addr); + } + }); + + uint16_t seq = 0; + uint32_t ts = 1000; + for (int wave = 0; wave < kWaves; wave++) { + int prev_n = 0; + for (int i = 0; i < kPerWave; i++) { + ut40_feed_pkt_anc0(ctx_, seq++, ts, 1, MTL_SESSION_PORT_P); + ts += 1000; + int n = ut40_captured_count(); + if (n > prev_n) { + void* addr = ut40_captured_addr(n - 1); + if (addr) { + std::lock_guard lock(mu); + ready_addrs.push(addr); + } + prev_n = n; + } + } + /* drain this wave's releases before reusing the capture buffer */ + for (;;) { + std::lock_guard lock(mu); + if (ready_addrs.empty()) break; + } + ut40_captured_reset(); + } + stop.store(true, std::memory_order_relaxed); + releaser.join(); + + EXPECT_EQ(ut40_stat_anc_frames_ready(ctx_) + ut40_stat_anc_frames_dropped(ctx_), + (uint64_t)(kWaves * kPerWave)) + << "every fed frame must be accounted as delivered or dropped exactly once"; +} + +/* ============================================================================ + * seq_lost / seq_discont / port_seq_* derived from per-frame bitmap. + * ========================================================================= */ + +class St40RxBitmapSeqStatsTest : public ::testing::Test { + protected: + ut_test_ctx* ctx_ = nullptr; + static constexpr uint16_t kSlots = 8; + static constexpr uint32_t kSlotSize = 4096; + + void SetUp() override { + ASSERT_EQ(ut40_init(), 0); + ut40_drain_ring(); + ut40_notify_rtp_calls_reset(); + ut40_captured_reset(); + ctx_ = ut40_ctx_create(2); + ASSERT_NE(ctx_, nullptr); + ut40_setup_frame_pool(ctx_, kSlots, kSlotSize); + } + void TearDown() override { + ut40_teardown_frame_pool(ctx_); + ut40_drain_ring(); + ut40_ctx_destroy(ctx_); + ctx_ = nullptr; + } +}; + +/* No loss -> seq_lost=0, seq_discont=false, status=COMPLETE. */ +TEST_F(St40RxBitmapSeqStatsTest, InOrderFrameNoLoss) { + for (uint16_t s = 100; s < 105; s++) { + bool marker = (s == 104); + ut40_feed_pkt_anc0(ctx_, s, 7000, marker ? 1 : 0, MTL_SESSION_PORT_P); + } + ASSERT_EQ(ut40_captured_count(), 1); + EXPECT_EQ(ut40_captured_seq_lost(0), 0u); + EXPECT_FALSE(ut40_captured_seq_discont(0)); + EXPECT_EQ(ut40_captured_port_seq_lost(0, MTL_SESSION_PORT_P), 0u); + EXPECT_FALSE(ut40_captured_port_seq_discont(0, MTL_SESSION_PORT_P)); + EXPECT_EQ(ut40_captured_port_pkts_recv(0, MTL_SESSION_PORT_P), 5u); + EXPECT_EQ(ut40_captured_pkts_total(0), 5u); + EXPECT_EQ(ut40_captured_status(0), ST_FRAME_STATUS_COMPLETE); +} + +/* Single-port gap of 1 -> seq_lost=1, seq_discont=true, status=CORRUPTED. */ +TEST_F(St40RxBitmapSeqStatsTest, SinglePortGapDetected) { + ut40_feed_pkt_anc0(ctx_, 200, 8000, 0, MTL_SESSION_PORT_P); + /* 201 missing */ + ut40_feed_pkt_anc0(ctx_, 202, 8000, 0, MTL_SESSION_PORT_P); + ut40_feed_pkt_anc0(ctx_, 203, 8000, 1, MTL_SESSION_PORT_P); + ASSERT_EQ(ut40_captured_count(), 1); + EXPECT_EQ(ut40_captured_seq_lost(0), 1u); + EXPECT_TRUE(ut40_captured_seq_discont(0)); + EXPECT_EQ(ut40_captured_port_seq_lost(0, MTL_SESSION_PORT_P), 1u); + EXPECT_TRUE(ut40_captured_port_seq_discont(0, MTL_SESSION_PORT_P)); + EXPECT_EQ(ut40_captured_status(0), ST_FRAME_STATUS_CORRUPTED); +} + +/* Multi-port gap on P recovered by R -> port_seq_lost[P]>0 but + * session seq_lost==0 (this is the entire point of the refactor). */ +TEST_F(St40RxBitmapSeqStatsTest, RedundancyMasksSessionLossButRecordsPortGap) { + /* P sends seqs 300, 302, 303 (302 lost on P). R sends only the missing 301. */ + ut40_feed_pkt_anc0(ctx_, 300, 9000, 0, MTL_SESSION_PORT_P); + ut40_feed_pkt_anc0(ctx_, 302, 9000, 0, MTL_SESSION_PORT_P); + ut40_feed_pkt_anc0(ctx_, 301, 9000, 0, MTL_SESSION_PORT_R); + ut40_feed_pkt_anc0(ctx_, 303, 9000, 1, MTL_SESSION_PORT_P); + ASSERT_EQ(ut40_captured_count(), 1); + EXPECT_EQ(ut40_captured_seq_lost(0), 0u) << "redundancy must mask session-level loss"; + EXPECT_FALSE(ut40_captured_seq_discont(0)); + EXPECT_EQ(ut40_captured_status(0), ST_FRAME_STATUS_COMPLETE); + EXPECT_GT(ut40_captured_port_seq_lost(0, MTL_SESSION_PORT_P), 0u) + << "P-port gap must still be reported per-port"; + EXPECT_TRUE(ut40_captured_port_seq_discont(0, MTL_SESSION_PORT_P)); + EXPECT_EQ(ut40_captured_port_seq_lost(0, MTL_SESSION_PORT_R), 0u); + EXPECT_FALSE(ut40_captured_port_seq_discont(0, MTL_SESSION_PORT_R)); + EXPECT_EQ(ut40_captured_port_pkts_recv(0, MTL_SESSION_PORT_P), 3u); + EXPECT_EQ(ut40_captured_port_pkts_recv(0, MTL_SESSION_PORT_R), 1u); +} + +/* Loss on BOTH ports for the same seq -> session seq_lost>0. */ +TEST_F(St40RxBitmapSeqStatsTest, UnrecoveredLossShowsSessionLost) { + ut40_feed_pkt_anc0(ctx_, 400, 10000, 0, MTL_SESSION_PORT_P); + ut40_feed_pkt_anc0(ctx_, 400, 10000, 0, MTL_SESSION_PORT_R); /* dup filtered */ + /* 401 lost on BOTH */ + ut40_feed_pkt_anc0(ctx_, 402, 10000, 0, MTL_SESSION_PORT_P); + ut40_feed_pkt_anc0(ctx_, 403, 10000, 1, MTL_SESSION_PORT_P); + ASSERT_EQ(ut40_captured_count(), 1); + EXPECT_EQ(ut40_captured_seq_lost(0), 1u); + EXPECT_TRUE(ut40_captured_seq_discont(0)); + EXPECT_EQ(ut40_captured_status(0), ST_FRAME_STATUS_CORRUPTED); +} + +/* No false-positive seq_discont across frame boundary: previous frame + * ended at seq=499, new frame starts at seq=500 (different tmstamp). + * The new frame's bitmap is fresh -> no loss reported. This is exactly + * the bug the refactor fixes. */ +TEST_F(St40RxBitmapSeqStatsTest, FrameBoundaryDoesNotInjectFalseLoss) { + ut40_feed_pkt_anc0(ctx_, 498, 11000, 0, MTL_SESSION_PORT_P); + ut40_feed_pkt_anc0(ctx_, 499, 11000, 1, MTL_SESSION_PORT_P); + ut40_feed_pkt_anc0(ctx_, 500, 11001, 0, MTL_SESSION_PORT_P); + ut40_feed_pkt_anc0(ctx_, 501, 11001, 1, MTL_SESSION_PORT_P); + ASSERT_EQ(ut40_captured_count(), 2); + EXPECT_EQ(ut40_captured_seq_lost(0), 0u); + EXPECT_EQ(ut40_captured_seq_lost(1), 0u); + EXPECT_FALSE(ut40_captured_seq_discont(0)); + EXPECT_FALSE(ut40_captured_seq_discont(1)); +} + +/* Reorder within the same frame must NOT show as loss. */ +TEST_F(St40RxBitmapSeqStatsTest, IntraFrameReorderNotLoss) { + ut40_feed_pkt_anc0(ctx_, 600, 12000, 0, MTL_SESSION_PORT_P); + ut40_feed_pkt_anc0(ctx_, 602, 12000, 0, MTL_SESSION_PORT_P); /* arrives early */ + ut40_feed_pkt_anc0(ctx_, 601, 12000, 0, MTL_SESSION_PORT_P); /* late but in-frame */ + ut40_feed_pkt_anc0(ctx_, 603, 12000, 1, MTL_SESSION_PORT_P); + ASSERT_EQ(ut40_captured_count(), 1); + EXPECT_EQ(ut40_captured_seq_lost(0), 0u) + << "intra-frame reorder must not be counted as loss"; + EXPECT_FALSE(ut40_captured_seq_discont(0)); + /* Per-port: seqs arrived 600,602,601,603 -> 600->602 has gap of 1 then + * 601 arrives backward; per-port discont gets set on the 600->602 jump. */ + EXPECT_TRUE(ut40_captured_port_seq_discont(0, MTL_SESSION_PORT_P)); +} + +/* Backward late arrival on R recovers a P gap detected before it arrived. */ +TEST_F(St40RxBitmapSeqStatsTest, BackwardLateRecoveryFlipsBit) { + /* P sends 700, 702, 703 (701 lost). Then R delivers 701 LATE (after 703). */ + ut40_feed_pkt_anc0(ctx_, 700, 13000, 0, MTL_SESSION_PORT_P); + ut40_feed_pkt_anc0(ctx_, 702, 13000, 0, MTL_SESSION_PORT_P); + ut40_feed_pkt_anc0(ctx_, 703, 13000, 1, + MTL_SESSION_PORT_P); /* marker, but 701 still missing */ + ASSERT_EQ(ut40_captured_count(), 1); + /* Without the late R arrival the marker delivers immediately, so the + * "backward recovery" must be tested via the multi-port pending path + * (covered in MultiPortForceDeliversStalePending). Here we only verify + * that under single-port the gap IS reported. */ + EXPECT_EQ(ut40_captured_seq_lost(0), 1u); + EXPECT_TRUE(ut40_captured_seq_discont(0)); +} diff --git a/tests/unit/session/st40/header_validation_test.cpp b/tests/unit/session/st40/header_validation_test.cpp index 652c78123..0b3194086 100644 --- a/tests/unit/session/st40/header_validation_test.cpp +++ b/tests/unit/session/st40/header_validation_test.cpp @@ -15,6 +15,10 @@ class St40RxHeaderValidationTest : public St40RxBaseTest {}; +TEST_F(St40RxHeaderValidationTest, FrameLevelOpsDoNotInferRtpLevel) { + EXPECT_EQ(ut40_ops_check_zero_init_with_rtp_callback(), -EINVAL); +} + /* Accepted packet must return 0. */ TEST_F(St40RxHeaderValidationTest, ReturnValueAccepted) { int rc = feed(0, 1000, false, MTL_SESSION_PORT_P); diff --git a/tests/unit/session/st40/rfc8331_codec_test.cpp b/tests/unit/session/st40/rfc8331_codec_test.cpp new file mode 100644 index 000000000..2da2e0633 --- /dev/null +++ b/tests/unit/session/st40/rfc8331_codec_test.cpp @@ -0,0 +1,222 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * Pin st40_rfc8331_encode_packet() / st40_rfc8331_decode_packet() against + * the RFC 8331 wire format, and the bswap helpers' host/network symmetry. + * + * Build: meson setup build_unit -Denable_unit_tests=true && ninja -C build_unit + * Run: ./build_unit/tests/unit/UnitTest --gtest_filter='St40Rfc8331*.*' + */ + +#include + +#include +#include +#include + +#include "st40_api.h" + +namespace { + +constexpr uint32_t kBufRoom = 1024; + +std::vector make_udw(uint16_t n, uint8_t seed) { + std::vector v(n); + for (uint16_t i = 0; i < n; i++) v[i] = static_cast(seed + i); + return v; +} + +struct st40_meta make_meta(uint16_t did, uint16_t sdid, uint16_t line, uint16_t c, + uint16_t s, uint16_t stream, uint16_t udw_size) { + struct st40_meta m {}; + m.c = c; + m.line_number = line; + m.hori_offset = 0x2CD; + m.s = s; + m.stream_num = stream; + m.did = did; + m.sdid = sdid; + m.udw_size = udw_size; + m.udw_offset = 0; + return m; +} + +} // namespace + +class St40Rfc8331CodecTest : public ::testing::Test {}; + +TEST_F(St40Rfc8331CodecTest, RoundTripMatrix) { + struct Case { + uint16_t did, sdid, line, c, s, stream, udw_size; + }; + const Case cases[] = { + {0x61, 0x02, 0, 0, 0, 0, 0}, {0x61, 0x02, 0x1AB, 1, 1, 7, 1}, + {0x41, 0x01, 260, 0, 1, 1, 8}, {0x08, 0x08, 0x1FF, 1, 0, 127, 9}, + {0x00, 0x00, 1, 1, 1, 1, 100}, {0xFF, 0xFF, 0x7FF, 1, 1, 0x7F, 255}, + }; + uint8_t seed = 0; + for (const auto& tc : cases) { + auto in = make_meta(tc.did, tc.sdid, tc.line, tc.c, tc.s, tc.stream, tc.udw_size); + auto udw = make_udw(tc.udw_size, seed++); + + std::vector buf(kBufRoom, 0); + uint32_t written = 0; + ASSERT_EQ(st40_rfc8331_encode_packet(buf.data(), kBufRoom, &in, udw.data(), &written), + 0) + << "encode udw_size=" << tc.udw_size; + EXPECT_EQ(written, st40_rfc8331_payload_bytes(tc.udw_size)); + + struct st40_meta out {}; + std::vector udw_out(tc.udw_size ? tc.udw_size : 1, 0xFF); + uint32_t consumed = 0; + ASSERT_EQ( + st40_rfc8331_decode_packet(buf.data(), written, &out, udw_out.data(), + static_cast(udw_out.size()), &consumed), + ST40_RFC8331_DECODE_OK) + << "decode udw_size=" << tc.udw_size; + EXPECT_EQ(consumed, written); + EXPECT_EQ(out.c, in.c); + EXPECT_EQ(out.line_number, in.line_number); + EXPECT_EQ(out.hori_offset, in.hori_offset); + EXPECT_EQ(out.s, in.s); + EXPECT_EQ(out.stream_num, in.stream_num); + EXPECT_EQ(out.did, in.did); + EXPECT_EQ(out.sdid, in.sdid); + EXPECT_EQ(out.udw_size, tc.udw_size); + for (uint16_t i = 0; i < tc.udw_size; i++) { + EXPECT_EQ(udw_out[i], udw[i]) << "udw mismatch sz=" << tc.udw_size << " i=" << i; + } + } +} + +TEST_F(St40Rfc8331CodecTest, DecodeShortBufferHeader) { + uint8_t tiny[3] = {0}; + struct st40_meta out {}; + uint32_t consumed = 0; + EXPECT_EQ(st40_rfc8331_decode_packet(tiny, sizeof(tiny), &out, nullptr, 0, &consumed), + ST40_RFC8331_DECODE_SHORT_BUFFER); +} + +TEST_F(St40Rfc8331CodecTest, DecodeShortBufferBody) { + auto in = make_meta(0x61, 0x02, 0x1AB, 1, 1, 7, 20); + auto udw = make_udw(20, 2); + std::vector buf(kBufRoom, 0); + uint32_t written = 0; + ASSERT_EQ(st40_rfc8331_encode_packet(buf.data(), kBufRoom, &in, udw.data(), &written), + 0); + struct st40_meta out {}; + uint32_t consumed = 0; + /* feed only the 8-byte header -- body is missing */ + EXPECT_EQ(st40_rfc8331_decode_packet(buf.data(), 8, &out, nullptr, 0, &consumed), + ST40_RFC8331_DECODE_SHORT_BUFFER); +} + +TEST_F(St40Rfc8331CodecTest, DecodeShortBufferUdwOut) { + auto in = make_meta(0x61, 0x02, 0x1AB, 1, 1, 7, 10); + auto udw = make_udw(10, 3); + std::vector buf(kBufRoom, 0); + uint32_t written = 0; + ASSERT_EQ(st40_rfc8331_encode_packet(buf.data(), kBufRoom, &in, udw.data(), &written), + 0); + struct st40_meta out {}; + std::vector small_out(5, 0); + uint32_t consumed = 0; + EXPECT_EQ(st40_rfc8331_decode_packet(buf.data(), written, &out, small_out.data(), 5, + &consumed), + ST40_RFC8331_DECODE_SHORT_BUFFER); +} + +TEST_F(St40Rfc8331CodecTest, DecodeParityFail) { + auto in = make_meta(0x61, 0x02, 0x1AB, 1, 1, 7, 4); + auto udw = make_udw(4, 4); + std::vector buf(kBufRoom, 0); + uint32_t written = 0; + ASSERT_EQ(st40_rfc8331_encode_packet(buf.data(), kBufRoom, &in, udw.data(), &written), + 0); + /* Corrupt a DID/SDID/DC parity bit in the second header chunk (first 4 bytes + * of the body). */ + buf[4] ^= 0x01; + struct st40_meta out {}; + std::vector udw_out(4, 0); + uint32_t consumed = 0; + EXPECT_EQ( + st40_rfc8331_decode_packet(buf.data(), written, &out, udw_out.data(), 4, &consumed), + ST40_RFC8331_DECODE_PARITY_FAIL); +} + +TEST_F(St40Rfc8331CodecTest, DecodeChecksumFail) { + auto in = make_meta(0x61, 0x02, 0x1AB, 1, 1, 7, 4); + auto udw = make_udw(4, 5); + std::vector buf(kBufRoom, 0); + uint32_t written = 0; + ASSERT_EQ(st40_rfc8331_encode_packet(buf.data(), kBufRoom, &in, udw.data(), &written), + 0); + /* Last 4 bytes hold the 10-bit checksum field; toggle a bit in the + * second-to-last byte (avoids the all-zero padding tail). */ + buf[written - 3] ^= 0x40; + struct st40_meta out {}; + std::vector udw_out(4, 0); + uint32_t consumed = 0; + EXPECT_EQ( + st40_rfc8331_decode_packet(buf.data(), written, &out, udw_out.data(), 4, &consumed), + ST40_RFC8331_DECODE_CHECKSUM_FAIL); +} + +/* RFC 8331 requires the checksum to be validated regardless of udw_size; + * a udw_size=0 sub-packet with a corrupted checksum must still be rejected. */ +TEST_F(St40Rfc8331CodecTest, DecodeChecksumFailZeroUdw) { + auto in = make_meta(0x61, 0x02, 0x1AB, 1, 1, 7, 0); + std::vector buf(kBufRoom, 0); + uint32_t written = 0; + ASSERT_EQ(st40_rfc8331_encode_packet(buf.data(), kBufRoom, &in, nullptr, &written), 0); + /* checksum word (idx=3) occupies body-relative bytes 3-4; corrupt the second byte. */ + buf[8] ^= 0x40; + struct st40_meta out {}; + uint32_t consumed = 0; + EXPECT_EQ(st40_rfc8331_decode_packet(buf.data(), written, &out, nullptr, 0, &consumed), + ST40_RFC8331_DECODE_CHECKSUM_FAIL); +} + +TEST_F(St40Rfc8331CodecTest, EncodeNoSpace) { + auto in = make_meta(0x61, 0x02, 0x1AB, 1, 1, 7, 10); + auto udw = make_udw(10, 6); + uint8_t small[8] = {0}; + uint32_t written = 0xDEAD; + EXPECT_EQ(st40_rfc8331_encode_packet(small, sizeof(small), &in, udw.data(), &written), + -ENOSPC); +} + +/* data_count is an 8-bit wire field; udw_size > 255 must be rejected rather + * than silently wrapping in the encoded header. */ +TEST_F(St40Rfc8331CodecTest, EncodeUdwSizeTooLarge) { + auto in = make_meta(0x61, 0x02, 0x1AB, 1, 1, 7, 256); + auto udw = make_udw(256, 7); + std::vector buf(kBufRoom, 0); + uint32_t written = 0xDEAD; + EXPECT_EQ(st40_rfc8331_encode_packet(buf.data(), kBufRoom, &in, udw.data(), &written), + -EINVAL); +} + +TEST_F(St40Rfc8331CodecTest, RtpHdrBswapRoundTrip) { + struct st40_rfc8331_rtp_hdr hdr {}; + hdr.swapped_first_hdr_chunk = 0x12345678u; + uint32_t orig = hdr.swapped_first_hdr_chunk; + st40_rfc8331_rtp_hdr_bswap(&hdr); + EXPECT_NE(hdr.swapped_first_hdr_chunk, orig); + st40_rfc8331_rtp_hdr_bswap(&hdr); + EXPECT_EQ(hdr.swapped_first_hdr_chunk, orig); +} + +TEST_F(St40Rfc8331CodecTest, PayloadHdrBswapRoundTrip) { + struct st40_rfc8331_payload_hdr hdr {}; + hdr.swapped_first_hdr_chunk = 0x12345678u; + hdr.swapped_second_hdr_chunk = 0x9ABCDEF0u; + uint32_t orig_first = hdr.swapped_first_hdr_chunk; + uint32_t orig_second = hdr.swapped_second_hdr_chunk; + st40_rfc8331_payload_hdr_bswap(&hdr); + EXPECT_NE(hdr.swapped_first_hdr_chunk, orig_first); + EXPECT_NE(hdr.swapped_second_hdr_chunk, orig_second); + st40_rfc8331_payload_hdr_bswap(&hdr); + EXPECT_EQ(hdr.swapped_first_hdr_chunk, orig_first); + EXPECT_EQ(hdr.swapped_second_hdr_chunk, orig_second); +} diff --git a/tests/unit/session/st40/rfc8331_payload_bytes_test.cpp b/tests/unit/session/st40/rfc8331_payload_bytes_test.cpp new file mode 100644 index 000000000..c80f55b25 --- /dev/null +++ b/tests/unit/session/st40/rfc8331_payload_bytes_test.cpp @@ -0,0 +1,45 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * Pin the wire-aligned byte size returned by st40_rfc8331_payload_bytes() + * against hand-computed RFC 8331 §2.1 values. + * + * Build: meson setup build_unit -Denable_unit_tests=true && ninja -C build_unit + * Run: ./build_unit/tests/unit/UnitTest --gtest_filter='St40Rfc8331PayloadBytesTest.*' + */ + +#include + +#include "st40_api.h" + +class St40Rfc8331PayloadBytesTest : public ::testing::Test {}; + +/* Values computed by hand from (ceil((3+udw+1)*10 / 8) aligned to 4) + 4. */ +TEST_F(St40Rfc8331PayloadBytesTest, MatchesRfc8331Formula) { + EXPECT_EQ(st40_rfc8331_payload_bytes(0), 12u); + EXPECT_EQ(st40_rfc8331_payload_bytes(1), 12u); + EXPECT_EQ(st40_rfc8331_payload_bytes(8), 20u); + EXPECT_EQ(st40_rfc8331_payload_bytes(9), 24u); + EXPECT_EQ(st40_rfc8331_payload_bytes(100), 136u); + EXPECT_EQ(st40_rfc8331_payload_bytes(255), 328u); +} + +/* tests/tools/RxTxApp/src/{rx,tx}_ancillary_app.c's pre-fix formula: floor + * division (missing the +7 before /8) then an align step that adds 4 bytes + * even when already 4-byte aligned. Pinned here so this exact pattern is + * never reintroduced: it diverges from the canonical helper (by 4 bytes, + * always too large) whenever udw_size % 16 == 12. */ +TEST_F(St40Rfc8331PayloadBytesTest, LegacyRxTxAppFormulaDivergedAtMod16Eq12) { + for (int udw = 0; udw <= 255; udw++) { + uint32_t total_size = (uint32_t)((3 + udw + 1) * 10) / 8; + total_size = (4 - total_size % 4) + total_size; + uint32_t legacy_len = + (uint32_t)(sizeof(struct st40_rfc8331_payload_hdr) - 4) + total_size; + uint32_t canonical_len = st40_rfc8331_payload_bytes((uint16_t)udw); + if (udw % 16 == 12) { + EXPECT_EQ(legacy_len, canonical_len + 4) << "udw_size=" << udw; + } else { + EXPECT_EQ(legacy_len, canonical_len) << "udw_size=" << udw; + } + } +} diff --git a/tests/unit/session/st40/tx_corrupt_parity_test.cpp b/tests/unit/session/st40/tx_corrupt_parity_test.cpp new file mode 100644 index 000000000..ad15f3d3d --- /dev/null +++ b/tests/unit/session/st40/tx_corrupt_parity_test.cpp @@ -0,0 +1,92 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * Pin tx_ancillary_corrupt_parity() (st_tx_ancillary_test.h, exercised via + * ut40_tx_corrupt_parity()): the BAD_PARITY fault-injection post-pass that + * strips parity bits from an already-encoded RFC 8331 sub-packet and + * recomputes its checksum, so decode fails on parity specifically, not + * checksum. + * + * Build: meson setup build_unit -Denable_unit_tests=true && ninja -C build_unit + * Run: ./build_unit/tests/unit/UnitTest --gtest_filter='St40TxCorruptParityTest.*' + */ + +#include + +#include +#include + +#include "session/st40_tx_test_harness.h" +#include "st40_api.h" + +namespace { + +constexpr uint32_t kBufRoom = 1024; + +std::vector make_udw(uint16_t n, uint8_t seed) { + std::vector v(n); + for (uint16_t i = 0; i < n; i++) v[i] = static_cast(seed + i); + return v; +} + +struct st40_meta make_meta(uint16_t did, uint16_t sdid, uint16_t udw_size) { + struct st40_meta m {}; + m.did = did; + m.sdid = sdid; + m.udw_size = udw_size; + return m; +} + +} // namespace + +class St40TxCorruptParityTest : public ::testing::Test {}; + +TEST_F(St40TxCorruptParityTest, DecodeFailsOnParityNotChecksum) { + auto meta = make_meta(0x61, 0x02, 8); + auto udw = make_udw(8, 5); + std::vector buf(kBufRoom, 0); + uint32_t written = 0; + ASSERT_EQ(st40_rfc8331_encode_packet(buf.data(), kBufRoom, &meta, udw.data(), &written), + 0); + + ut40_tx_corrupt_parity(buf.data(), meta.udw_size); + + struct st40_meta out {}; + std::vector udw_out(8, 0); + uint32_t consumed = 0; + EXPECT_EQ(st40_rfc8331_decode_packet(buf.data(), written, &out, udw_out.data(), + static_cast(udw_out.size()), &consumed), + ST40_RFC8331_DECODE_PARITY_FAIL); +} + +TEST_F(St40TxCorruptParityTest, ChecksumStaysInternallyConsistent) { + auto meta = make_meta(0x41, 0x01, 4); + auto udw = make_udw(4, 1); + std::vector buf(kBufRoom, 0); + uint32_t written = 0; + ASSERT_EQ(st40_rfc8331_encode_packet(buf.data(), kBufRoom, &meta, udw.data(), &written), + 0); + + ut40_tx_corrupt_parity(buf.data(), meta.udw_size); + + auto* ph = reinterpret_cast(buf.data()); + uint8_t* udw_dst = reinterpret_cast(&ph->second_hdr_chunk); + uint16_t stored = st40_get_udw(3 + meta.udw_size, udw_dst); + uint16_t recomputed = st40_calc_checksum(3 + meta.udw_size, udw_dst); + EXPECT_EQ(stored, recomputed); +} + +TEST_F(St40TxCorruptParityTest, ZeroUdwSizeStillCorrupted) { + auto meta = make_meta(0x08, 0x08, 0); + std::vector buf(kBufRoom, 0); + uint32_t written = 0; + ASSERT_EQ(st40_rfc8331_encode_packet(buf.data(), kBufRoom, &meta, nullptr, &written), + 0); + + ut40_tx_corrupt_parity(buf.data(), meta.udw_size); + + struct st40_meta out {}; + uint32_t consumed = 0; + EXPECT_EQ(st40_rfc8331_decode_packet(buf.data(), written, &out, nullptr, 0, &consumed), + ST40_RFC8331_DECODE_PARITY_FAIL); +} diff --git a/tests/unit/session/st40_harness.c b/tests/unit/session/st40_harness.c index d81809d8c..c93e07bb3 100644 --- a/tests/unit/session/st40_harness.c +++ b/tests/unit/session/st40_harness.c @@ -21,12 +21,19 @@ #include "common/ut_common.h" #include "st2110/st_rx_ancillary_session.c" +_Static_assert( + __builtin_types_compatible_p(typeof(((struct st_rx_anc_frame_slot*)0)->state), + _Atomic enum st_rx_anc_slot_state), + "frame_slots[].state must be a C11 _Atomic enum: it is read/written from " + "both the RX tasklet and the app thread (st40_rx_put_framebuff)"); + /* ── define the opaque context ────────────────────────────────────────── */ struct ut_test_ctx { struct mtl_main_impl impl; struct st_rx_ancillary_sessions_mgr mgr; struct st_rx_ancillary_session_impl session; + struct mt_ptp_impl ptp_storage; }; /* pull in the header after the struct definition so types resolve */ @@ -35,12 +42,22 @@ struct ut_test_ctx { /* ── globals ──────────────────────────────────────────────────────────── */ static struct rte_ring* g_ring; -static bool g_skip_drain; /* when true, notify_rtp_ready does not drain the ring */ +static bool g_skip_drain; /* when true, notify_rtp_ready does not drain the ring */ +static int g_notify_rtp_calls; /* count to verify dispatch suppression */ + +/* ── PTP time stub ────────────────────────────────────────────────────── */ -/* ── RTP ready callback ──────────────────────────────────────────────── */ +static uint64_t ut40_ptp_time(struct mtl_main_impl* impl, enum mtl_port port) { + (void)port; + impl->ptp_usync += 1000; + return impl->ptp_usync; +} + +/* ── RTP ready callback ───────────────────────────────────── */ static int ut_notify_rtp_ready(void* priv) { (void)priv; + g_notify_rtp_calls++; if (!g_skip_drain) ut_ring_drain(g_ring); return 0; } @@ -57,6 +74,21 @@ int ut40_init(void) { return 0; } +int ut40_ops_check_zero_init_with_rtp_callback(void) { + struct st40_rx_ops ops; + + memset(&ops, 0, sizeof(ops)); + ops.num_port = 1; + ops.ip_addr[0][0] = 239; + ops.ip_addr[0][1] = 1; + ops.ip_addr[0][2] = 1; + ops.ip_addr[0][3] = 1; + ops.rtp_ring_size = UT_RING_SIZE; + ops.notify_rtp_ready = ut_notify_rtp_ready; + + return rx_ancillary_ops_check(&ops); +} + /* ── context create / destroy ─────────────────────────────────────────── */ ut_test_ctx* ut40_ctx_create(int num_port) { @@ -67,6 +99,11 @@ ut_test_ctx* ut40_ctx_create(int num_port) { ctx->impl.type = MT_HANDLE_MAIN; ctx->impl.tsc_hz = rte_get_tsc_hz(); + for (int i = 0; i < MTL_PORT_MAX; i++) { + ctx->impl.inf[i].parent = &ctx->impl; + ctx->impl.inf[i].port = i; + ctx->impl.inf[i].ptp_get_time_fn = ut40_ptp_time; + } ctx->mgr.parent = &ctx->impl; ctx->mgr.idx = 0; @@ -79,6 +116,7 @@ ut_test_ctx* ut40_ctx_create(int num_port) { ctx->session.ops.num_port = num_port; ctx->session.ops.payload_type = 0; ctx->session.ops.interlaced = false; + ctx->session.ops.type = ST40_TYPE_RTP_LEVEL; /* legacy default for harness */ ctx->session.ops.rtp_ring_size = UT_RING_SIZE; ctx->session.ops.notify_rtp_ready = ut_notify_rtp_ready; ctx->session.ops.priv = &ctx->session; @@ -216,6 +254,15 @@ void ut40_ctx_set_interlace_auto(ut_test_ctx* ctx, bool enable) { ctx->session.interlace_auto = enable; } +void ut40_ctx_enable_hw_timestamp(ut_test_ctx* ctx, enum mtl_session_port port) { + enum mtl_port phy = mt_port_logic2phy(ctx->session.port_maps, port); + ctx->impl.dynfield_offset = ut_register_hw_rx_timestamp(); + ctx->ptp_storage.coefficient = 1.0; + ctx->ptp_storage.last_sync_ts = 0; + ctx->impl.ptp[phy] = &ctx->ptp_storage; + ctx->impl.inf[phy].feature |= MT_IF_FEATURE_RX_OFFLOAD_TIMESTAMP; +} + /* ── stat accessors ───────────────────────────────────────────────────── */ uint64_t ut40_stat_unrecovered(const ut_test_ctx* ctx) { @@ -304,6 +351,358 @@ void ut40_set_skip_drain(bool skip) { g_skip_drain = skip; } +void ut40_set_frame_level(ut_test_ctx* ctx) { + ctx->session.ops.type = ST40_TYPE_FRAME_LEVEL; + /* Dispatch-only stub; no frame_slots pool needed since it does not touch + * them. Use ut40_setup_frame_pool() for tests that need the slot pool. */ +} + +uint64_t ut40_stat_assemble_dispatched(const ut_test_ctx* ctx) { + return ctx->session.stat_assemble_dispatched; +} + +int ut40_notify_rtp_calls(void) { + return g_notify_rtp_calls; +} +void ut40_notify_rtp_calls_reset(void) { + g_notify_rtp_calls = 0; +} + +/* ── FRAME_LEVEL test helpers ─────────────────────────────────────────── */ + +#define UT40_CAPTURE_MAX 32 +#define UT40_CAPTURE_META_PER_FRAME ST40_MAX_META + +struct ut40_captured_frame { + void* addr; + uint16_t meta_num; + uint32_t udw_buffer_fill; + uint32_t rtp_timestamp; + uint64_t timestamp_first_pkt; + bool rtp_marker; + bool interlaced; + bool second_field; + uint32_t seq_lost; + bool seq_discont; + uint32_t port_seq_lost[MTL_SESSION_PORT_MAX]; + bool port_seq_discont[MTL_SESSION_PORT_MAX]; + uint32_t pkts_recv[MTL_SESSION_PORT_MAX]; + uint32_t pkts_total; + int status; + /* deep copy of meta[] so caller can release the slot before inspecting */ + struct st40_meta meta[UT40_CAPTURE_META_PER_FRAME]; + /* deep copy of udw payload bytes (capped) */ + uint8_t udw_copy[1024]; + uint32_t udw_copy_len; +}; + +static struct ut40_captured_frame g_captured[UT40_CAPTURE_MAX]; +static int g_captured_count; +static int g_notify_frame_fail_after = -1; /* -1 = never fail */ + +static int ut_notify_frame_ready(void* priv, void* addr, + struct st40_rx_frame_meta* meta) { + (void)priv; + if (g_notify_frame_fail_after >= 0 && g_captured_count >= g_notify_frame_fail_after) + return -1; + if (g_captured_count >= UT40_CAPTURE_MAX) return 0; /* silently drop overflow */ + + struct ut40_captured_frame* c = &g_captured[g_captured_count++]; + c->addr = addr; + c->meta_num = meta->meta_num; + c->udw_buffer_fill = meta->udw_buffer_fill; + c->rtp_timestamp = meta->rtp_timestamp; + c->timestamp_first_pkt = meta->timestamp_first_pkt; + c->rtp_marker = meta->rtp_marker; + c->interlaced = meta->interlaced; + c->second_field = meta->second_field; + if (meta->meta && meta->meta_num <= UT40_CAPTURE_META_PER_FRAME) + memcpy(c->meta, meta->meta, sizeof(struct st40_meta) * meta->meta_num); + c->udw_copy_len = meta->udw_buffer_fill < sizeof(c->udw_copy) ? meta->udw_buffer_fill + : sizeof(c->udw_copy); + if (addr) memcpy(c->udw_copy, addr, c->udw_copy_len); + c->seq_lost = meta->seq_lost; + c->seq_discont = meta->seq_discont; + c->pkts_total = meta->pkts_total; + c->status = meta->status; + for (int i = 0; i < MTL_SESSION_PORT_MAX; i++) { + c->port_seq_lost[i] = meta->port_seq_lost[i]; + c->port_seq_discont[i] = meta->port_seq_discont[i]; + c->pkts_recv[i] = meta->pkts_recv[i]; + } + return 0; +} + +void ut40_setup_frame_pool(ut_test_ctx* ctx, uint16_t framebuff_cnt, + uint32_t framebuff_size) { + ctx->session.ops.type = ST40_TYPE_FRAME_LEVEL; + ctx->session.ops.framebuff_cnt = framebuff_cnt; + ctx->session.ops.framebuff_size = framebuff_size; + ctx->session.ops.notify_frame_ready = ut_notify_frame_ready; + rx_ancillary_session_init_frames(&ctx->session); +} + +void ut40_teardown_frame_pool(ut_test_ctx* ctx) { + rx_ancillary_session_uinit_frames(&ctx->session); + g_captured_count = 0; + g_notify_frame_fail_after = -1; +} + +int ut40_captured_count(void) { + return g_captured_count; +} +void ut40_captured_reset(void) { + g_captured_count = 0; +} +void* ut40_captured_addr(int i) { + return (i >= 0 && i < g_captured_count) ? g_captured[i].addr : NULL; +} +uint16_t ut40_captured_meta_num(int i) { + return (i >= 0 && i < g_captured_count) ? g_captured[i].meta_num : 0; +} +uint32_t ut40_captured_udw_fill(int i) { + return (i >= 0 && i < g_captured_count) ? g_captured[i].udw_buffer_fill : 0; +} +uint32_t ut40_captured_rtp_ts(int i) { + return (i >= 0 && i < g_captured_count) ? g_captured[i].rtp_timestamp : 0; +} +uint64_t ut40_captured_timestamp_first_pkt(int i) { + return (i >= 0 && i < g_captured_count) ? g_captured[i].timestamp_first_pkt : 0; +} +bool ut40_captured_marker(int i) { + return (i >= 0 && i < g_captured_count) ? g_captured[i].rtp_marker : false; +} +bool ut40_captured_interlaced(int i) { + return (i >= 0 && i < g_captured_count) ? g_captured[i].interlaced : false; +} +int ut40_captured_meta_did(int frame_i, int meta_i) { + if (frame_i < 0 || frame_i >= g_captured_count) return -1; + if (meta_i < 0 || meta_i >= UT40_CAPTURE_META_PER_FRAME) return -1; + return g_captured[frame_i].meta[meta_i].did; +} +int ut40_captured_meta_sdid(int frame_i, int meta_i) { + if (frame_i < 0 || frame_i >= g_captured_count) return -1; + if (meta_i < 0 || meta_i >= UT40_CAPTURE_META_PER_FRAME) return -1; + return g_captured[frame_i].meta[meta_i].sdid; +} +int ut40_captured_meta_udw_size(int frame_i, int meta_i) { + if (frame_i < 0 || frame_i >= g_captured_count) return -1; + if (meta_i < 0 || meta_i >= UT40_CAPTURE_META_PER_FRAME) return -1; + return g_captured[frame_i].meta[meta_i].udw_size; +} +uint32_t ut40_captured_meta_udw_offset(int frame_i, int meta_i) { + if (frame_i < 0 || frame_i >= g_captured_count) return 0; + if (meta_i < 0 || meta_i >= UT40_CAPTURE_META_PER_FRAME) return 0; + return g_captured[frame_i].meta[meta_i].udw_offset; +} +uint8_t ut40_captured_udw_byte(int frame_i, uint32_t off) { + if (frame_i < 0 || frame_i >= g_captured_count) return 0; + if (off >= g_captured[frame_i].udw_copy_len) return 0; + return g_captured[frame_i].udw_copy[off]; +} + +uint32_t ut40_captured_seq_lost(int i) { + return (i >= 0 && i < g_captured_count) ? g_captured[i].seq_lost : 0; +} +bool ut40_captured_seq_discont(int i) { + return (i >= 0 && i < g_captured_count) ? g_captured[i].seq_discont : false; +} +uint32_t ut40_captured_port_seq_lost(int i, enum mtl_session_port p) { + if (i < 0 || i >= g_captured_count) return 0; + if ((int)p < 0 || (int)p >= MTL_SESSION_PORT_MAX) return 0; + return g_captured[i].port_seq_lost[p]; +} +bool ut40_captured_port_seq_discont(int i, enum mtl_session_port p) { + if (i < 0 || i >= g_captured_count) return false; + if ((int)p < 0 || (int)p >= MTL_SESSION_PORT_MAX) return false; + return g_captured[i].port_seq_discont[p]; +} +uint32_t ut40_captured_port_pkts_recv(int i, enum mtl_session_port p) { + if (i < 0 || i >= g_captured_count) return 0; + if ((int)p < 0 || (int)p >= MTL_SESSION_PORT_MAX) return 0; + return g_captured[i].pkts_recv[p]; +} +uint32_t ut40_captured_pkts_total(int i) { + return (i >= 0 && i < g_captured_count) ? g_captured[i].pkts_total : 0; +} +int ut40_captured_status(int i) { + return (i >= 0 && i < g_captured_count) ? g_captured[i].status : -1; +} + +void ut40_set_notify_frame_fail_after(int n) { + g_notify_frame_fail_after = n; +} + +void ut40_release_frame(ut_test_ctx* ctx, void* addr) { + struct st_rx_ancillary_session_handle_impl handle = { + .type = MT_HANDLE_RX_ANC, + .impl = &ctx->session, + }; + st40_rx_put_framebuff(&handle, addr); +} + +uint64_t ut40_stat_anc_frames_ready(const ut_test_ctx* ctx) { + return ctx->session.stat_anc_frames_ready; +} +uint64_t ut40_stat_anc_frames_dropped(const ut_test_ctx* ctx) { + return ctx->session.stat_anc_frames_dropped; +} +uint64_t ut40_stat_anc_pkt_parse_err(const ut_test_ctx* ctx) { + return ctx->session.stat_anc_pkt_parse_err; +} + +/* Build + feed an mbuf carrying one real ANC packet (parity + checksum + * applied by harness). Use corrupt_parity_word>=0 to flip parity bits on + * that UDW word; corrupt_checksum=true to bit-flip the checksum word. */ +int ut40_feed_anc_pkt(ut_test_ctx* ctx, uint16_t seq, uint32_t ts, int marker, + enum mtl_session_port port, uint8_t did, uint8_t sdid, + const uint8_t* udw_bytes, uint16_t udw_size, + int corrupt_parity_word, bool corrupt_checksum) { + struct rte_mbuf* m = rte_pktmbuf_alloc(ut_pool()); + if (!m) return -1; + + size_t hdr_offset = sizeof(struct st_rfc3550_hdr) - sizeof(struct st_rfc3550_rtp_hdr); + uint32_t total_bits = (uint32_t)(3 + udw_size + 1) * 10; + uint32_t udw_payload_size = (total_bits + 7) / 8; + udw_payload_size = (udw_payload_size + 3) & ~0x3U; + size_t anc_pkt_bytes = sizeof(struct st40_rfc8331_payload_hdr) - 4 + udw_payload_size; + size_t total = hdr_offset + sizeof(struct st40_rfc8331_rtp_hdr) + anc_pkt_bytes; + + if (rte_pktmbuf_tailroom(m) < total) { + rte_pktmbuf_free(m); + return -1; + } + + uint8_t* buf = rte_pktmbuf_mtod(m, uint8_t*); + memset(buf, 0, total); + + /* RTP base + first chunk (anc_count = 1, f=0) */ + struct st40_rfc8331_rtp_hdr* rtp = (struct st40_rfc8331_rtp_hdr*)(buf + hdr_offset); + rtp->base.version = 2; + rtp->base.seq_number = htons(seq); + rtp->base.tmstamp = htonl(ts); + rtp->base.payload_type = 0; + rtp->base.marker = marker ? 1 : 0; + uint32_t chunk = ((uint32_t)1) << 24; /* anc_count = 1, f=0 */ + rtp->swapped_first_hdr_chunk = htonl(chunk); + + /* ANC payload */ + struct st40_rfc8331_payload_hdr* pkt = + (struct st40_rfc8331_payload_hdr*)((uint8_t*)rtp + sizeof(*rtp)); + pkt->first_hdr_chunk.c = 0; + pkt->first_hdr_chunk.line_number = 9; + pkt->first_hdr_chunk.horizontal_offset = 0; + pkt->first_hdr_chunk.s = 0; + pkt->first_hdr_chunk.stream_num = 0; + pkt->second_hdr_chunk.did = st40_add_parity_bits(did); + pkt->second_hdr_chunk.sdid = st40_add_parity_bits(sdid); + pkt->second_hdr_chunk.data_count = st40_add_parity_bits((uint8_t)udw_size); + st40_rfc8331_payload_hdr_bswap(pkt); + + uint8_t* udw_dst = (uint8_t*)&pkt->second_hdr_chunk; + for (uint16_t i = 0; i < udw_size; i++) { + uint16_t v = st40_add_parity_bits(udw_bytes[i]); + if (corrupt_parity_word == (int)i) v ^= 0x300; /* flip parity bits */ + st40_set_udw(i + 3, v, udw_dst); + } + uint16_t cs = st40_calc_checksum(3 + udw_size, udw_dst); + if (corrupt_checksum) cs ^= 0x1; + st40_set_udw(udw_size + 3, cs, udw_dst); + + m->data_len = total; + m->pkt_len = total; + + int rc = rx_ancillary_session_handle_pkt(&ctx->impl, &ctx->session, m, port); + rte_pktmbuf_free(m); + return rc; +} + +/* Framing-only feed: one well-formed empty (udw_size=0) ANC packet. Use for + * slot-lifecycle tests where ANC content doesn't matter but the parser must + * still accept the packet. */ +int ut40_feed_pkt_anc0(ut_test_ctx* ctx, uint16_t seq, uint32_t ts, int marker, + enum mtl_session_port port) { + return ut40_feed_anc_pkt(ctx, seq, ts, marker, port, 0x41, 0x05, NULL, 0, -1, false); +} + +int ut40_feed_pkt_anc0_hw_ts(ut_test_ctx* ctx, uint16_t seq, uint32_t ts, int marker, + enum mtl_session_port port, uint64_t hw_raw_ns) { + struct rte_mbuf* m = make_anc_mbuf(seq, ts, marker); + if (!m) return -1; + ut_mbuf_set_hw_timestamp(m, ctx->impl.dynfield_offset, hw_raw_ns); + int rc = rx_ancillary_session_handle_pkt(&ctx->impl, &ctx->session, m, port); + rte_pktmbuf_free(m); + return rc; +} + +static uint32_t ut40_anc_payload_bytes(uint16_t udw_size) { + return st40_rfc8331_payload_bytes(udw_size); +} + +int ut40_feed_multi_anc_pkt(ut_test_ctx* ctx, uint16_t seq, uint32_t ts, int marker, + enum mtl_session_port port, const uint16_t* udw_sizes, + uint8_t anc_count) { + if (!udw_sizes || !anc_count || anc_count > ST40_MAX_META) return -1; + + size_t hdr_offset = sizeof(struct st_rfc3550_hdr) - sizeof(struct st_rfc3550_rtp_hdr); + size_t payload_len = 0; + for (uint8_t anc_idx = 0; anc_idx < anc_count; anc_idx++) + payload_len += ut40_anc_payload_bytes(udw_sizes[anc_idx]); + size_t total = hdr_offset + sizeof(struct st40_rfc8331_rtp_hdr) + payload_len; + + struct rte_mbuf* m = rte_pktmbuf_alloc(ut_pool()); + if (!m) return -1; + if (rte_pktmbuf_tailroom(m) < total) { + rte_pktmbuf_free(m); + return -1; + } + + uint8_t* buf = rte_pktmbuf_mtod(m, uint8_t*); + memset(buf, 0, total); + + struct st40_rfc8331_rtp_hdr* rtp = (struct st40_rfc8331_rtp_hdr*)(buf + hdr_offset); + rtp->base.version = 2; + rtp->base.seq_number = htons(seq); + rtp->base.tmstamp = htonl(ts); + rtp->base.marker = marker ? 1 : 0; + uint32_t chunk = ((uint32_t)anc_count) << 24; + rtp->swapped_first_hdr_chunk = htonl(chunk); + + uint8_t* payload = (uint8_t*)(rtp + 1); + for (uint8_t anc_idx = 0; anc_idx < anc_count; anc_idx++) { + uint16_t udw_size = udw_sizes[anc_idx]; + struct st40_rfc8331_payload_hdr* payload_hdr = + (struct st40_rfc8331_payload_hdr*)payload; + + payload_hdr->first_hdr_chunk.c = 0; + payload_hdr->first_hdr_chunk.line_number = 10 + anc_idx; + payload_hdr->first_hdr_chunk.horizontal_offset = 0; + payload_hdr->first_hdr_chunk.s = 0; + payload_hdr->first_hdr_chunk.stream_num = 0; + payload_hdr->second_hdr_chunk.did = st40_add_parity_bits(0x41); + payload_hdr->second_hdr_chunk.sdid = st40_add_parity_bits(0x05); + payload_hdr->second_hdr_chunk.data_count = st40_add_parity_bits((uint8_t)udw_size); + st40_rfc8331_payload_hdr_bswap(payload_hdr); + + uint8_t* udw_dst = (uint8_t*)&payload_hdr->second_hdr_chunk; + for (uint16_t udw_idx = 0; udw_idx < udw_size; udw_idx++) { + uint8_t v = (uint8_t)(((uint16_t)(anc_idx + 1) * 17 + udw_idx) & 0xff); + st40_set_udw(udw_idx + 3, st40_add_parity_bits(v), udw_dst); + } + uint16_t checksum = st40_calc_checksum(3 + udw_size, udw_dst); + st40_set_udw(udw_size + 3, checksum, udw_dst); + + payload += ut40_anc_payload_bytes(udw_size); + } + + m->data_len = total; + m->pkt_len = total; + + int rc = rx_ancillary_session_handle_pkt(&ctx->impl, &ctx->session, m, port); + rte_pktmbuf_free(m); + return rc; +} + void ut40_session_reset(ut_test_ctx* ctx) { rx_ancillary_session_reset(&ctx->session, false); } diff --git a/tests/unit/session/st40_harness.h b/tests/unit/session/st40_harness.h index c71609970..08808ea2b 100644 --- a/tests/unit/session/st40_harness.h +++ b/tests/unit/session/st40_harness.h @@ -37,6 +37,9 @@ typedef struct ut_test_ctx ut_test_ctx; * safe to call once per gtest fixture SetUp(). Returns 0 on success. */ int ut40_init(void); +/* Validate zero-initialized RX ops that only configure the RTP callback. */ +int ut40_ops_check_zero_init_with_rtp_callback(void); + /* Create a context with `num_port` enabled (1 = single-port, 2 = redundant). * Caller owns the returned pointer and must free it with ut40_ctx_destroy(). */ ut_test_ctx* ut40_ctx_create(int num_port); @@ -98,6 +101,12 @@ void ut40_ctx_set_ssrc(ut_test_ctx* ctx, uint32_t ssrc); * session enforces its configured value. */ void ut40_ctx_set_interlace_auto(ut_test_ctx* ctx, bool enable); +/* Enable the HW RX-timestamp offload path on `port`: registers the DPDK + * dynfield, installs an identity-mapped PTP correction, and sets + * MT_IF_FEATURE_RX_OFFLOAD_TIMESTAMP so mt_mbuf_time_stamp() reads the + * mbuf dynfield instead of falling back to the software PTP clock. */ +void ut40_ctx_enable_hw_timestamp(ut_test_ctx* ctx, enum mtl_session_port port); + /* Wrapper feeder — drives the production `_handle_mbuf` wrapper instead of * the per-packet handler. Use this whenever the test asserts on per-port * `err_packets` or per-port `packets`. */ @@ -176,6 +185,78 @@ void ut40_invoke_rx_ancillary_session_stat(ut_test_ctx* ctx); */ int ut40_ring_dequeue_markers(int* out_count, bool* out_has_marker); +/* Switch session to FRAME_LEVEL transport dispatch. After this returns, + * subsequent ut40_feed_* calls bypass the rtp ring and go to the assembler + * stub. Stats (received, port frames, seq tracking) still update. */ +void ut40_set_frame_level(ut_test_ctx* ctx); +uint64_t ut40_stat_assemble_dispatched(const ut_test_ctx* ctx); +int ut40_notify_rtp_calls(void); +void ut40_notify_rtp_calls_reset(void); + +/* Full FRAME_LEVEL setup with allocated slot pool + capturing notify + * callback. After this, ut40_feed_* drives full assembly; delivered frames + * are captured and inspectable via ut40_captured_*. Tests must call + * ut40_release_frame() (or ut40_teardown_frame_pool) to recycle slots. */ +void ut40_setup_frame_pool(ut_test_ctx* ctx, uint16_t framebuff_cnt, + uint32_t framebuff_size); +void ut40_teardown_frame_pool(ut_test_ctx* ctx); + +int ut40_captured_count(void); +void ut40_captured_reset(void); +void* ut40_captured_addr(int i); +uint16_t ut40_captured_meta_num(int i); +uint32_t ut40_captured_udw_fill(int i); +uint32_t ut40_captured_rtp_ts(int i); +uint64_t ut40_captured_timestamp_first_pkt(int i); +bool ut40_captured_marker(int i); +bool ut40_captured_interlaced(int i); +int ut40_captured_meta_did(int frame_i, int meta_i); +int ut40_captured_meta_sdid(int frame_i, int meta_i); +int ut40_captured_meta_udw_size(int frame_i, int meta_i); +uint32_t ut40_captured_meta_udw_offset(int frame_i, int meta_i); +uint8_t ut40_captured_udw_byte(int frame_i, uint32_t off); + +/* seq-stat accessors */ +uint32_t ut40_captured_seq_lost(int i); +bool ut40_captured_seq_discont(int i); +uint32_t ut40_captured_port_seq_lost(int i, enum mtl_session_port p); +bool ut40_captured_port_seq_discont(int i, enum mtl_session_port p); +uint32_t ut40_captured_port_pkts_recv(int i, enum mtl_session_port p); +uint32_t ut40_captured_pkts_total(int i); +int ut40_captured_status(int i); + +void ut40_set_notify_frame_fail_after(int n); +void ut40_release_frame(ut_test_ctx* ctx, void* addr); + +uint64_t ut40_stat_anc_frames_ready(const ut_test_ctx* ctx); +uint64_t ut40_stat_anc_frames_dropped(const ut_test_ctx* ctx); +uint64_t ut40_stat_anc_pkt_parse_err(const ut_test_ctx* ctx); + +/* Build + feed an mbuf carrying one real ANC packet (parity + checksum + * applied by harness). Use corrupt_parity_word>=0 to flip parity bits on + * that UDW word; corrupt_checksum=true to bit-flip the checksum word. */ +int ut40_feed_anc_pkt(ut_test_ctx* ctx, uint16_t seq, uint32_t ts, int marker, + enum mtl_session_port port, uint8_t did, uint8_t sdid, + const uint8_t* udw_bytes, uint16_t udw_size, + int corrupt_parity_word, bool corrupt_checksum); + +/* Framing-only feed: well-formed empty ANC packet (udw_size=0). */ +int ut40_feed_pkt_anc0(ut_test_ctx* ctx, uint16_t seq, uint32_t ts, int marker, + enum mtl_session_port port); + +/* Same as ut40_feed_pkt_anc0() but stamps the mbuf's HW RX-timestamp + * dynfield with `hw_raw_ns` before feeding it. Requires a prior + * ut40_ctx_enable_hw_timestamp(). */ +int ut40_feed_pkt_anc0_hw_ts(ut_test_ctx* ctx, uint16_t seq, uint32_t ts, int marker, + enum mtl_session_port port, uint64_t hw_raw_ns); + +/* Build + feed one RTP packet carrying multiple RFC 8331 ANC data packets + * (non-split mode), each with its own UDW size from `udw_sizes`. Exercises + * the per-ANC payload stride advance inside rx_anc_slot_parse_pkt(). */ +int ut40_feed_multi_anc_pkt(ut_test_ctx* ctx, uint16_t seq, uint32_t ts, int marker, + enum mtl_session_port port, const uint16_t* udw_sizes, + uint8_t anc_count); + #ifdef __cplusplus } diff --git a/tests/unit/session/st40_tx_test_harness.c b/tests/unit/session/st40_tx_test_harness.c new file mode 100644 index 000000000..dba1bffbf --- /dev/null +++ b/tests/unit/session/st40_tx_test_harness.c @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + */ + +#include "session/st40_tx_test_harness.h" + +#include "st2110/st_tx_ancillary_session.h" +#include "st2110/st_tx_ancillary_test.h" + +void ut40_tx_corrupt_parity(uint8_t* buf, uint16_t udw_size) { + tx_ancillary_corrupt_parity(buf, udw_size); +} diff --git a/tests/unit/session/st40_tx_test_harness.h b/tests/unit/session/st40_tx_test_harness.h new file mode 100644 index 000000000..0df64d266 --- /dev/null +++ b/tests/unit/session/st40_tx_test_harness.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * C harness API exposing the ST 2110-40 (ancillary) TX BAD_PARITY + * corrective-transform (tx_ancillary_corrupt_parity(), st_tx_ancillary_test.h) + * to the C++ gtest layer. + */ + +#ifndef _ST40_TX_TEST_HARNESS_H_ +#define _ST40_TX_TEST_HARNESS_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Wraps tx_ancillary_corrupt_parity(): strips parity bits from did/sdid/ + * data_count/each UDW word of the RFC 8331 sub-packet at `buf` and + * recomputes its checksum in place. */ +void ut40_tx_corrupt_parity(uint8_t* buf, uint16_t udw_size); + +#ifdef __cplusplus +} +#endif + +#endif