From 90225c191f2dd6dc309af6c7dca23fe9124f59ab Mon Sep 17 00:00:00 2001 From: Rony Rado Date: Sun, 29 Jun 2025 20:19:27 +0000 Subject: [PATCH 1/6] feat(advanced_network_media) Add Advanced Network Media operators - Add AdvNetworkMediaRxOp for receiving media streams over Rivermax - Supports packet-to-frame conversion with ANO Burst processing - Configurable video formats, bit depths, and frame dimensions - Output as VideoBuffer or Tensor entities - Add AdvNetworkMediaTxOp for transmitting media streams over Rivermax - Processes VideoBuffer/Tensor inputs for network transmission - Configurable network interface and queue management - Support for various video formats Features: - SMPTE 2110 compliance for professional media over IP - GPU acceleration and GPUDirect support - Low latency optimizations - Python bindings for both operators These operators build upon the Advanced Network library to provide specialized functionality for broadcast and media streaming use cases requiring strict timing and high throughput performance. Signed-off-by: Rony Rado --- operators/CMakeLists.txt | 1 + .../advanced_network_media/CMakeLists.txt | 33 + operators/advanced_network_media/README.md | 851 ++++++++++++++++++ .../advanced_network_media_rx/CMakeLists.txt | 78 ++ .../adv_network_media_rx.cpp | 797 ++++++++++++++++ .../adv_network_media_rx.h | 76 ++ .../frame_assembly_controller.cpp | 480 ++++++++++ .../frame_assembly_controller.h | 288 ++++++ .../frame_provider.h | 53 ++ .../media_frame_assembler.cpp | 693 ++++++++++++++ .../media_frame_assembler.h | 348 +++++++ .../memory_copy_strategies.cpp | 685 ++++++++++++++ .../memory_copy_strategies.h | 369 ++++++++ .../advanced_network_media_rx/metadata.json | 32 + .../network_burst_processor.cpp | 131 +++ .../network_burst_processor.h | 73 ++ .../python/CMakeLists.txt | 46 + .../python/adv_network_media_rx_pybind.cpp | 139 +++ .../python/adv_network_media_rx_pydoc.hpp | 101 +++ .../advanced_network_media_tx/CMakeLists.txt | 57 ++ .../adv_network_media_tx.cpp | 318 +++++++ .../adv_network_media_tx.h | 72 ++ .../advanced_network_media_tx/metadata.json | 32 + .../python/CMakeLists.txt | 46 + .../python/adv_network_media_tx_pybind.cpp | 124 +++ .../python/adv_network_media_tx_pydoc.hpp | 92 ++ .../common/CMakeLists.txt | 37 + .../common/adv_network_media_common.h | 41 + .../common/adv_network_media_logging.h | 232 +++++ .../common/frame_buffer.cpp | 274 ++++++ .../common/frame_buffer.h | 254 ++++++ .../common/rtp_params.h | 85 ++ .../common/video_parameters.cpp | 146 +++ .../common/video_parameters.h | 102 +++ 34 files changed, 7186 insertions(+) create mode 100644 operators/advanced_network_media/CMakeLists.txt create mode 100644 operators/advanced_network_media/README.md create mode 100644 operators/advanced_network_media/advanced_network_media_rx/CMakeLists.txt create mode 100644 operators/advanced_network_media/advanced_network_media_rx/adv_network_media_rx.cpp create mode 100644 operators/advanced_network_media/advanced_network_media_rx/adv_network_media_rx.h create mode 100644 operators/advanced_network_media/advanced_network_media_rx/frame_assembly_controller.cpp create mode 100644 operators/advanced_network_media/advanced_network_media_rx/frame_assembly_controller.h create mode 100644 operators/advanced_network_media/advanced_network_media_rx/frame_provider.h create mode 100644 operators/advanced_network_media/advanced_network_media_rx/media_frame_assembler.cpp create mode 100644 operators/advanced_network_media/advanced_network_media_rx/media_frame_assembler.h create mode 100644 operators/advanced_network_media/advanced_network_media_rx/memory_copy_strategies.cpp create mode 100644 operators/advanced_network_media/advanced_network_media_rx/memory_copy_strategies.h create mode 100644 operators/advanced_network_media/advanced_network_media_rx/metadata.json create mode 100644 operators/advanced_network_media/advanced_network_media_rx/network_burst_processor.cpp create mode 100644 operators/advanced_network_media/advanced_network_media_rx/network_burst_processor.h create mode 100644 operators/advanced_network_media/advanced_network_media_rx/python/CMakeLists.txt create mode 100644 operators/advanced_network_media/advanced_network_media_rx/python/adv_network_media_rx_pybind.cpp create mode 100644 operators/advanced_network_media/advanced_network_media_rx/python/adv_network_media_rx_pydoc.hpp create mode 100644 operators/advanced_network_media/advanced_network_media_tx/CMakeLists.txt create mode 100644 operators/advanced_network_media/advanced_network_media_tx/adv_network_media_tx.cpp create mode 100644 operators/advanced_network_media/advanced_network_media_tx/adv_network_media_tx.h create mode 100644 operators/advanced_network_media/advanced_network_media_tx/metadata.json create mode 100644 operators/advanced_network_media/advanced_network_media_tx/python/CMakeLists.txt create mode 100644 operators/advanced_network_media/advanced_network_media_tx/python/adv_network_media_tx_pybind.cpp create mode 100644 operators/advanced_network_media/advanced_network_media_tx/python/adv_network_media_tx_pydoc.hpp create mode 100644 operators/advanced_network_media/common/CMakeLists.txt create mode 100644 operators/advanced_network_media/common/adv_network_media_common.h create mode 100644 operators/advanced_network_media/common/adv_network_media_logging.h create mode 100644 operators/advanced_network_media/common/frame_buffer.cpp create mode 100644 operators/advanced_network_media/common/frame_buffer.h create mode 100644 operators/advanced_network_media/common/rtp_params.h create mode 100644 operators/advanced_network_media/common/video_parameters.cpp create mode 100644 operators/advanced_network_media/common/video_parameters.h diff --git a/operators/CMakeLists.txt b/operators/CMakeLists.txt index 730ffe5138..49243ce01a 100644 --- a/operators/CMakeLists.txt +++ b/operators/CMakeLists.txt @@ -15,6 +15,7 @@ # Add operators (in alphabetical order) add_holohub_operator(advanced_network) +add_subdirectory(advanced_network_media) add_holohub_operator(aja_source) add_holohub_operator(apriltag_detector) add_holohub_operator(basic_network) diff --git a/operators/advanced_network_media/CMakeLists.txt b/operators/advanced_network_media/CMakeLists.txt new file mode 100644 index 0000000000..ecca87af59 --- /dev/null +++ b/operators/advanced_network_media/CMakeLists.txt @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required(VERSION 3.20) + +# Find holoscan package (required for common library and operators) +find_package(holoscan 2.6 QUIET CONFIG + PATHS "/opt/nvidia/holoscan" "/workspace/holoscan-sdk/install") + +# Only build if holoscan is found +if(holoscan_FOUND) + # Build common code first + add_subdirectory(common) + + # Build operator-specific code + add_holohub_operator(advanced_network_media_rx) + add_holohub_operator(advanced_network_media_tx) +else() + message(STATUS "Holoscan SDK not found - skipping advanced_network_media operators") +endif() + diff --git a/operators/advanced_network_media/README.md b/operators/advanced_network_media/README.md new file mode 100644 index 0000000000..8090e42d23 --- /dev/null +++ b/operators/advanced_network_media/README.md @@ -0,0 +1,851 @@ +# Advanced Network Media Operators + +This directory contains operators for high-performance media streaming over advanced network infrastructure. The operators provide efficient transmission and reception of media frames (such as video) using NVIDIA's Rivermax SDK and other high-performance networking technologies. + +> [!NOTE] +> These operators build upon the [Advanced Network library](../advanced_network/README.md) to provide specialized functionality for media streaming applications. They are designed for professional broadcast and media streaming use cases that require strict timing and high throughput. + +## Operators + +The Advanced Network Media library provides two main operators: + +### `holoscan::ops::AdvNetworkMediaRxOp` + +Operator for receiving media frames over advanced network infrastructure. This operator receives video frames over Rivermax-enabled network infrastructure and outputs them as GXF VideoBuffer or Tensor entities. + +**Inputs** +- None (receives data directly from network interface via Advanced Network Manager library) + +**Outputs** +- **`output`**: Video frames as GXF entities (VideoBuffer or Tensor) + - type: `gxf::Entity` + +**Parameters** +- **`interface_name`**: Name of the network interface to use for receiving + - type: `std::string` +- **`queue_id`**: Queue ID for the network interface (default: 0) + - type: `uint16_t` +- **`frame_width`**: Width of incoming video frames in pixels + - type: `uint32_t` +- **`frame_height`**: Height of incoming video frames in pixels + - type: `uint32_t` +- **`bit_depth`**: Bit depth of the video format + - type: `uint32_t` +- **`video_format`**: Video format specification (e.g., "RGB888", "YUV422") + - type: `std::string` +- **`hds`**: Indicates if header-data split mode is enabled in the input data + - type: `bool` +- **`output_format`**: Output format for the received frames ("video_buffer" for VideoBuffer, "tensor" for Tensor) + - type: `std::string` +- **`memory_location`**: Memory location for frame buffers ("device", "host", etc.) + - type: `std::string` + +### `holoscan::ops::AdvNetworkMediaTxOp` + +Operator for transmitting media frames over advanced network infrastructure. This operator processes video frames from GXF entities (either VideoBuffer or Tensor) and transmits them over Rivermax-enabled network infrastructure. + +**Inputs** +- **`input`**: Video frames as GXF entities (VideoBuffer or Tensor) + - type: `gxf::Entity` + +**Outputs** +- None (transmits data directly to network interface) + +**Parameters** +- **`interface_name`**: Name of the network interface to use for transmission + - type: `std::string` +- **`queue_id`**: Queue ID for the network interface (default: 0) + - type: `uint16_t` +- **`video_format`**: Video format specification (e.g., "RGB888", "YUV422") + - type: `std::string` +- **`bit_depth`**: Bit depth of the video format + - type: `uint32_t` +- **`frame_width`**: Width of video frames to transmit in pixels + - type: `uint32_t` +- **`frame_height`**: Height of video frames to transmit in pixels + - type: `uint32_t` + +## Requirements + +- All requirements from the [Advanced Network library](../advanced_network/README.md) +- NVIDIA Rivermax SDK +- Compatible video formats and frame rates +- Proper network configuration for media streaming + +## Features + +- **High-performance media streaming**: Optimized for professional broadcast applications +- **SMPTE 2110 compliance**: Supports industry-standard media over IP protocols +- **Low latency**: Direct hardware access minimizes processing delays +- **GPU acceleration**: Supports GPUDirect for zero-copy operations +- **Flexible formats**: Support for various video formats and bit depths +- **Header-data split**: Optimized memory handling for improved performance + +## RX Data Flow Architecture + +When using Rivermax for media reception, the data flows through multiple optimized layers from network interface to the application. This section traces the complete RX path: + +### Complete RX Data Flow + +```mermaid +graph TD + %% Network Layer + A["Network Interface
(ConnectX NIC)"] --> B["Rivermax Hardware
Acceleration"] + B --> C{{"RDK Service Selection"}} + C -->|ipo_receiver| D["IPO Receiver Service
(rmax_ipo_receiver)"] + C -->|rtp_receiver| E["RTP Receiver Service
(rmax_rtp_receiver)"] + + %% Hardware to Memory + D --> F["Direct Memory Access
(DMA)"] + E --> F + F --> G["Pre-allocated Memory
Regions"] + + %% Advanced Network Manager Layer + G --> H["RivermaxMgr
(Advanced Network Manager)"] + H --> I["Burst Assembly
(burst_manager)"] + I --> J["RivermaxBurst Container
+ AnoBurstExtendedInfo"] + + %% Memory Layout Decision + J --> K{{"Header-Data Split
(HDS) Config"}} + K -->|HDS Enabled| L["Headers: CPU Memory
burst->pkts[0]
Payloads: GPU Memory
burst->pkts[1]"] + K -->|HDS Disabled| M["Headers + Payloads
CPU Memory
burst->pkts[0] + offset"] + + %% Queue Management + L --> N["AnoBurstsQueue
(Pointer Distribution)"] + M --> N + + %% Media RX Operator + N --> O["AdvNetworkMediaRxOp"] + O --> P["Burst Processor
(network_burst_processor)"] + P --> Q["Frame Assembly
(MediaFrameAssembler)"] + + %% Strategy Selection + Q --> R{{"Memory Layout
Analysis"}} + R -->|Contiguous| S["ContiguousStrategy
cudaMemcpy()"] + R -->|Strided| T["StridedStrategy
cudaMemcpy2D()"] + + %% Frame Assembly + S --> U["Frame Assembly
(Single Copy)"] + T --> U + U --> V["VideoBuffer/Tensor
Entity Creation"] + + %% Output + V --> W["Media Player Application"] + W --> X["HolovizOp
(Real-time Display)"] + W --> Y["FramesWriter
(File Output)"] + + %% Styling + classDef networkLayer fill:#e1f5fe + classDef managerLayer fill:#f3e5f5 + classDef operatorLayer fill:#e8f5e8 + classDef applicationLayer fill:#fff3e0 + classDef dataStructure fill:#ffebee + + class A,B,C,D,E,F,G networkLayer + class H,I,J,K,L,M,N managerLayer + class O,P,Q,R,S,T,U,V operatorLayer + class W,X,Y applicationLayer + class J,L,M dataStructure +``` + +### 1. Network Layer → Hardware Acceleration +``` +Network Interface (ConnectX NIC) → Rivermax Hardware Acceleration +├── IPO (Inline Packet Ordering) Receiver Service [OR] +├── RTP Receiver Service [OR] +└── Direct Memory Access (DMA) to pre-allocated buffers +``` + +**Key Components:** +- **ConnectX NIC**: NVIDIA ConnectX-6 or later with hardware streaming acceleration +- **Rivermax SDK (RDK) Services** (configured as one of the following): + - `rmax_ipo_receiver`: RX service for receiving RTP data using Rivermax Inline Packet Ordering (IPO) feature + - `rmax_rtp_receiver`: RX service for receiving RTP data using Rivermax Striding protocol +- **Hardware Features**: RDMA, GPUDirect, hardware timestamping for minimal latency + +> **Note**: The choice between IPO and RTP receiver services is determined by the `settings_type` configuration parameter (`"ipo_receiver"` or `"rtp_receiver"`). These services are provided by the Rivermax Development Kit (RDK). + +### 2. Advanced Network Manager Layer +``` +Rivermax Services → Advanced Network Manager (RivermaxMgr) +├── Configuration Management (rivermax_config_manager) +├── Service Management (rivermax_mgr_service) +├── Burst Management (burst_manager) +└── Memory Management (CPU/GPU memory regions) +``` + +**Key Responsibilities:** +- **Packet Reception**: Rivermax services receive packets from NIC hardware directly into pre-allocated memory regions +- **Burst Assembly**: Packet **pointers** are aggregated into `RivermaxBurst` containers for efficient processing (no data copying) +- **Memory Coordination**: Manages memory regions (host/device) and buffer allocation +- **Metadata Extraction**: Extract timing, flow, and RTP sequence information from packet headers +- **Burst Metadata**: Includes `AnoBurstExtendedInfo` with configuration details (HDS status, stride sizes, memory locations) +- **Queue Management**: Thread-safe queues (`AnoBurstsQueue`) for burst pointer distribution + +> **Zero-Copy Architecture**: Bursts contain only pointers to packets in memory, not the packet data itself. No memcpy operations are performed at the burst level. + +### 3. Advanced Network Media RX Operator +``` +Advanced Network Manager → AdvNetworkMediaRxOp +├── Burst Processing (network_burst_processor) +├── Frame Assembly (media_frame_assembler) +│ ├── Frame Assembly Controller (state management) +│ ├── Memory Copy Strategy Detection (ContiguousStrategy/StridedStrategy) +│ ├── Memory Copy Optimization (cudaMemcpy/cudaMemcpy2D) +│ └── RTP Sequence Validation +└── Frame Buffer Management (frame_buffer) +``` + +**Conversion Process:** +- **Burst Processing**: Receives burst containers with **packet pointers** from Advanced Network Manager (no data copying) +- **HDS-Aware Packet Access**: Extracts packet data based on Header-Data Split configuration: + - **HDS Enabled**: RTP headers from `CPU_PKTS` array (CPU memory), payloads from `GPU_PKTS` array (GPU memory) + - **HDS Disabled**: Both headers and payloads from `CPU_PKTS` array with RTP header offset +- **Dynamic Strategy Detection**: Analyzes packet memory layout via pointers to determine optimal copy strategy: + - `ContiguousStrategy`: For exactly contiguous packets in memory (single `cudaMemcpy` operation) + - `StridedStrategy`: For packets with memory gaps/strides (optimized `cudaMemcpy2D` with stride parameters) + - **Adaptive Behavior**: Strategy can switch mid-stream if memory layout changes (buffer wraparound, stride breaks) +- **Frame Assembly**: Converts RTP packet data (accessed via pointers) to complete video frames +- **Format Handling**: Supports RGB888, YUV420, NV12 with proper stride calculation +- **Memory Optimization**: Only one copy operation from packet memory to frame buffer (when needed) + +> **Key Point**: The burst processing layer works entirely with packet pointers. The only data copying occurs during frame assembly, directly from packet memory locations to final frame buffers. + +### 4. Media Player Application +``` +AdvNetworkMediaRxOp → Media Player Application +├── HolovizOp (Real-time Display) +├── FramesWriter (File Output) +└── Format Conversion (if needed) +``` + +**Output Options:** +- **Real-time Visualization**: Direct display using HolovizOp with minimal latency +- **File Output**: Save frames to disk for analysis or post-processing +- **Format Flexibility**: Output as GXF VideoBuffer or Tensor entities + +### Memory Flow Optimization + +The RX path is optimized for minimal memory copies through pointer-based architecture: + +``` +Network → NIC Hardware → Pre-allocated Memory Regions → Packet Pointers → Frame Assembly → Application + ↓ ↓ ↓ ↓ ↓ ↓ +ConnectX DMA Transfer CPU/GPU Buffers Burst Containers Single Copy Display/File + (pointers only) (if needed) +``` + +### Memory Architecture and HDS Configuration + +```mermaid +graph TB + subgraph "Network Hardware" + A["ConnectX NIC
Hardware"] + B["DMA Engine"] + A --> B + end + + subgraph "Memory Regions" + C["Pre-allocated
Memory Regions"] + D["CPU Memory
(Host)"] + E["GPU Memory
(Device)"] + C --> D + C --> E + end + + B --> C + + subgraph "HDS Enabled Configuration" + F["RTP Headers
CPU Memory"] + G["Payload Data
GPU Memory"] + H["burst->pkts[0]
(Header Pointers)"] + I["burst->pkts[1]
(Payload Pointers)"] + + D --> F + E --> G + F --> H + G --> I + end + + subgraph "HDS Disabled Configuration" + J["Headers + Payloads
Together"] + K["CPU Memory Only"] + L["burst->pkts[0]
(Combined Pointers)"] + M["RTP Header Offset
Calculation"] + + D --> J + J --> K + K --> L + L --> M + end + + subgraph "Burst Processing" + N["AnoBurstExtendedInfo
Metadata"] + O["header_stride_size
payload_stride_size
hds_on
payload_on_cpu"] + + N --> O + end + + H --> N + I --> N + L --> N + + subgraph "Copy Strategy Selection" + P{{"Memory Layout
Analysis"}} + Q["Contiguous
Strategy"] + R["Strided
Strategy"] + S["cudaMemcpy
(Single Block)"] + T["cudaMemcpy2D
(Strided Copy)"] + + N --> P + P -->|Contiguous| Q + P -->|Gaps/Strides| R + Q --> S + R --> T + end + + subgraph "Frame Assembly" + U["Frame Buffer
(Target)"] + V["Single Copy Operation
(Packet → Frame)"] + W["VideoBuffer/Tensor
Entity"] + + S --> V + T --> V + V --> U + U --> W + end + + %% Styling + classDef hardwareLayer fill:#e1f5fe + classDef memoryLayer fill:#f3e5f5 + classDef hdsEnabled fill:#e8f5e8 + classDef hdsDisabled fill:#fff3e0 + classDef metadataLayer fill:#ffebee + classDef processingLayer fill:#e3f2fd + classDef outputLayer fill:#f1f8e9 + + class A,B hardwareLayer + class C,D,E memoryLayer + class F,G,H,I hdsEnabled + class J,K,L,M hdsDisabled + class N,O metadataLayer + class P,Q,R,S,T processingLayer + class U,V,W outputLayer +``` + +**Memory Architecture:** +- **Direct DMA**: Packets arrive directly into pre-allocated memory regions via hardware DMA +- **Pointer Management**: Bursts contain only pointers to packet locations, no intermediate data copying +- **Single Copy Strategy**: Only one memory copy operation (from packet memory to frame buffer) when format conversion or memory location change is needed +- **Header-Data Split (HDS) Configuration**: + - **HDS Enabled**: Headers stored in CPU memory (`burst->pkts[0]`), payloads in GPU memory (`burst->pkts[1]`) + - **HDS Disabled**: Headers and payloads together in CPU memory (`burst->pkts[0]`) with RTP header offset +- **Memory Regions**: Configured via `AnoBurstExtendedInfo` metadata (header/payload locations, stride sizes) +- **True Zero-Copy Paths**: When packet memory and frame buffer are in same location and format, no copying required + +### Performance Characteristics + +- **Latency**: Sub-millisecond packet processing through hardware acceleration and pointer-based architecture +- **Throughput**: Multi-gigabit streaming with batched packet processing (no burst-level copying) +- **Efficiency**: Minimal CPU usage through hardware offload, GPU acceleration, and zero-copy pointer management +- **Memory Efficiency**: Maximum one copy operation from packet memory to frame buffer (often zero copies) +- **Scalability**: Multiple queues and CPU core isolation for parallel processing + +### Configuration Integration + +The RX path is configured through YAML files that specify: +- **Network Settings**: Interface addresses, IP addresses, ports, multicast groups +- **Memory Regions**: Buffer sizes, memory types (host/device), allocation strategies +- **Video Parameters**: Format, resolution, bit depth, frame rate +- **Rivermax Settings**: IPO/RTP receiver configuration, timing parameters, statistics + + +This architecture provides professional-grade media streaming with hardware-accelerated packet processing, pointer-based zero-copy optimization, and flexible output options suitable for broadcast and media production workflows. The key advantage is that packet data is never unnecessarily copied - bursts manage only pointers, and actual data movement occurs only once during frame assembly when needed. + +## Data Structures and Relationships + +Understanding the key data structures and their relationships is crucial for working with the Advanced Network Media operators: + +### Core Data Structures + +```mermaid +classDiagram + %% RX Data Structures + class BurstParams { + +BurstHeader hdr + +std::array~void**~ pkts + +std::array~uint32_t*~ pkt_lens + +void** pkt_extra_info + +std::shared_ptr~void~ custom_pkt_data + +cudaEvent_t event + } + + class RivermaxBurst { + +get_burst_info() AnoBurstExtendedInfo* + +get_port_id() uint16_t + +get_queue_id() uint16_t + +get_burst_id() uint16_t + +reset_burst_packets() void + } + + class AnoBurstExtendedInfo { + +uint32_t tag + +uint16_t burst_id + +bool hds_on + +uint16_t header_stride_size + +uint16_t payload_stride_size + +bool header_on_cpu + +bool payload_on_cpu + +uint16_t header_seg_idx + +uint16_t payload_seg_idx + } + + class MediaFrameAssembler { + -std::shared_ptr~IFrameProvider~ frame_provider_ + -std::shared_ptr~FrameBufferBase~ current_frame_ + -std::unique_ptr~IMemoryCopyStrategy~ copy_strategy_ + +process_incoming_packet(RtpParams, payload) void + +configure_burst_parameters() void + +set_completion_handler() void + +get_statistics() Statistics + } + + class IFrameProvider { + <> + +get_new_frame() FrameBufferBase* + +get_frame_size() size_t + +has_available_frames() bool + +return_frame_to_pool() void + } + + class FrameBufferBase { + <> + #MemoryLocation memory_location_ + #MemoryStorageType src_storage_type_ + #size_t frame_size_ + #Entity entity_ + +get() byte_t* + +get_size() size_t + +get_memory_location() MemoryLocation + } + + class VideoFrameBufferBase { + <> + #uint32_t width_ + #uint32_t height_ + #VideoFormat format_ + +validate_frame_parameters() Status + #validate_format_compliance() Status + } + + %% TX Data Structures + class VideoBufferFrameBuffer { + -Handle~VideoBuffer~ buffer_ + -std::vector~ColorPlane~ planes_ + +get() byte_t* + +wrap_in_entity() Entity + } + + class TensorFrameBuffer { + -Handle~Tensor~ tensor_ + +get() byte_t* + +wrap_in_entity() Entity + } + + class AllocatedVideoBufferFrameBuffer { + -void* data_ + +get() byte_t* + +wrap_in_entity() Entity + } + + class AllocatedTensorFrameBuffer { + -void* data_ + -uint32_t channels_ + +get() byte_t* + +wrap_in_entity() Entity + } + + %% RDK Service Classes (External SDK) + class MediaFrame { + <> + Note: Rivermax SDK class + } + + class MediaFramePool { + <> + Note: Rivermax SDK class + } + + class MediaSenderZeroCopyService { + -std::shared_ptr~BufferedMediaFrameProvider~ tx_media_frame_provider_ + -bool is_frame_in_process_ + +get_tx_packet_burst() Status + +send_tx_burst() Status + +is_tx_burst_available() bool + } + + class MediaSenderService { + -std::unique_ptr~MediaFramePool~ tx_media_frame_pool_ + -std::shared_ptr~BufferedMediaFrameProvider~ tx_media_frame_provider_ + -std::shared_ptr~MediaFrame~ processing_frame_ + +get_tx_packet_burst() Status + +send_tx_burst() Status + +is_tx_burst_available() bool + } + + %% Inheritance Relationships + RivermaxBurst --|> BurstParams : inherits + VideoFrameBufferBase --|> FrameBufferBase : inherits + VideoBufferFrameBuffer --|> VideoFrameBufferBase : inherits + TensorFrameBuffer --|> VideoFrameBufferBase : inherits + AllocatedVideoBufferFrameBuffer --|> VideoFrameBufferBase : inherits + AllocatedTensorFrameBuffer --|> VideoFrameBufferBase : inherits + + %% Composition and Association Relationships + RivermaxBurst *-- AnoBurstExtendedInfo : stores in hdr.custom_burst_data + RivermaxBurst ..> MediaFrameAssembler : processed by + MediaFrameAssembler ..> FrameBufferBase : creates + MediaFrameAssembler o-- IFrameProvider : uses + + BurstParams ..> MediaFrame : references via custom_pkt_data + BurstParams ..> FrameBufferBase : references via custom_pkt_data + + MediaSenderService *-- MediaFramePool : manages + MediaFramePool o-- MediaFrame : provides + + MediaSenderZeroCopyService ..> MediaFrame : uses directly + MediaSenderService ..> MediaFrame : copies to pool +``` + +## TX Data Flow Architecture + +When using Rivermax for media transmission, the data flows from application through frame-level processing layers to the RDK MediaSender service, which handles all packet processing internally. This section traces the complete TX path: + +### Complete TX Data Flow + +```mermaid +graph TD + %% Application Layer + A["Media Sender Application"] --> B["VideoBuffer/Tensor
GXF Entity"] + + %% Media TX Operator + B --> C["AdvNetworkMediaTxOp"] + C --> D["Frame Validation
& Format Check"] + D --> E["Frame Wrapping"] + E --> F{{"Input Type"}} + F -->|VideoBuffer| G["VideoBufferFrameBuffer
(Wrapper)"] + F -->|Tensor| H["TensorFrameBuffer
(Wrapper)"] + + %% MediaFrame Creation + G --> I["MediaFrame Creation
(Reference Only)"] + H --> I + I --> J["BurstParams Creation"] + J --> K["MediaFrame Attachment
(via custom_pkt_data)"] + + %% Advanced Network Manager + K --> L["RivermaxMgr
(Advanced Network Manager)"] + L --> M{{"Service Selection
(use_internal_memory_pool)"}} + + %% Service Paths + M -->|false| N["MediaSenderZeroCopyService
(True Zero-Copy)"] + M -->|true| O["MediaSenderService
(Single Copy + Pool)"] + + %% Zero-Copy Path + N --> P["No Internal Memory Pool"] + P --> Q["Direct Frame Reference
(custom_pkt_data)"] + Q --> R["RDK MediaSenderApp
(Zero Copy Processing)"] + + %% Memory Pool Path + O --> S["Pre-allocated MediaFramePool"] + S --> T["Single Memory Copy
(burst->pkts[0][0])"] + T --> U["RDK MediaSenderApp
(Pool Buffer Processing)"] + + %% RDK Processing (Common) + R --> V["RDK Internal Processing"] + U --> V + V --> W["RTP Packetization
(SMPTE 2110)"] + W --> X["Protocol Headers
& Metadata"] + X --> Y["Timing Control
& Scheduling"] + + %% Hardware Transmission + Y --> Z["Rivermax Hardware
Acceleration"] + Z --> AA["ConnectX NIC
Hardware Queues"] + AA --> BB["Network Interface
Transmission"] + + %% Cleanup Paths + R --> CC["Frame Ownership Transfer
& Release"] + T --> DD["Pool Buffer Reuse"] + + %% Styling + classDef applicationLayer fill:#fff3e0 + classDef operatorLayer fill:#e8f5e8 + classDef managerLayer fill:#f3e5f5 + classDef rdkLayer fill:#e3f2fd + classDef networkLayer fill:#e1f5fe + classDef dataStructure fill:#ffebee + classDef zeroCopyPath fill:#e8f5e8,stroke:#4caf50,stroke-width:3px + classDef poolPath fill:#fff8e1,stroke:#ff9800,stroke-width:3px + + class A,B applicationLayer + class C,D,E,F,G,H,I,J,K operatorLayer + class L,M managerLayer + class N,P,Q,R,O,S,T,U rdkLayer + class V,W,X,Y,Z,AA,BB networkLayer + class I,J,K,S dataStructure + class N,P,Q,R,CC zeroCopyPath + class O,S,T,U,DD poolPath +``` + +### 1. Media Sender Application → Advanced Network Media TX Operator +``` +Media Sender Application → AdvNetworkMediaTxOp +├── Video Frame Input (GXF VideoBuffer or Tensor entities) +├── Frame Validation and Wrapping +│ ├── VideoBufferFrameBuffer (for VideoBuffer input) +│ ├── TensorFrameBuffer (for Tensor input) +│ └── MediaFrame Creation (wrapper around frame buffer) +└── Frame Buffer Management (no packet processing) +``` + +**Frame Processing:** +- **Frame Reception**: Receives video frames as GXF VideoBuffer or Tensor entities from application +- **Format Validation**: Validates input format against configured video parameters (resolution, bit depth, format) +- **Frame Wrapping**: Creates MediaFrame wrapper around the original frame buffer data (no data copying) +- **Buffer Management**: Manages frame buffer lifecycle and memory references + +> **Key Point**: No packet processing occurs at this level. All operations work with complete video frames. + +### 2. Advanced Network Media TX Operator → Advanced Network Manager +``` +AdvNetworkMediaTxOp → Advanced Network Manager (RivermaxMgr) +├── Burst Parameter Creation +├── MediaFrame Attachment (via custom_pkt_data) +├── Service Coordination +└── Frame Handoff to RDK Services +``` + +**Frame Handoff Process:** +- **Burst Creation**: Creates burst parameters container (`BurstParams`) for transmission +- **Frame Attachment**: Attaches MediaFrame to burst via `custom_pkt_data` field (no data copying) +- **Service Routing**: Routes burst to appropriate MediaSender service based on configuration +- **Memory Management**: Coordinates frame buffer ownership transfer + +> **Zero-Copy Architecture**: MediaFrame objects contain only references to original frame buffer memory. No frame data copying occurs. + +### 3. Advanced Network Manager → Rivermax SDK (RDK) Services +``` +Advanced Network Manager → Rivermax SDK (RDK) Services +├── MediaSenderZeroCopyService (true zero-copy) [OR] +├── MediaSenderService (single copy + mempool) [OR] +├── MediaSenderMockService (testing mode) [OR] +└── Internal Frame Processing via MediaSenderApp +``` + +**RDK Service Paths:** + +#### MediaSenderZeroCopyService (True Zero-Copy Path) +- **No Internal Memory Pool**: Does not create internal frame buffers +- **Direct Frame Transfer**: Application's MediaFrame is passed directly to RDK via `custom_pkt_data` +- **Zero Memory Copies**: No memcpy operations - only ownership transfer of application's frame buffer +- **Frame Lifecycle**: After RDK processes the frame, it is released/destroyed (not reused) +- **Memory Efficiency**: Maximum efficiency - application frame buffer used directly by RDK + +#### MediaSenderService (Single Copy + Memory Pool Path) +- **Internal Memory Pool**: Creates pre-allocated `MediaFramePool` with `MEDIA_FRAME_POOL_SIZE` buffers +- **One Memory Copy**: Application's frame data is copied from `custom_pkt_data` to pool buffer via `burst->pkts[0][0]` +- **Pool Management**: Pool frames are reused after RDK finishes processing +- **Frame Lifecycle**: Pool frames are returned to memory pool for subsequent transmissions +- **Buffering**: Provides buffering capability for sustained high-throughput transmission + +#### MediaSenderMockService (Testing Mode) +- **Mock Implementation**: Testing service with minimal functionality +- **Single Pre-allocated Frame**: Uses one static frame buffer for testing + +> **Note**: The choice of MediaSender service depends on configuration (`use_internal_memory_pool`, `dummy_sender` flags). All services are provided by the Rivermax Development Kit (RDK). + +### 4. RDK MediaSender Service → Network Hardware +``` +MediaSenderApp (RDK) → Internal Processing → Hardware Transmission +├── Frame-to-Packet Conversion (RTP/SMPTE 2110) +├── Packet Timing and Scheduling +├── Hardware Queue Management +└── DMA to Network Interface +``` + +**RDK Internal Processing:** +- **Packetization**: RDK internally converts video frames to RTP packets following SMPTE 2110 standards +- **Timing Control**: Applies precise packet timing for synchronized transmission +- **Hardware Integration**: Direct submission to ConnectX NIC hardware queues via DMA +- **Network Transmission**: Packets transmitted with hardware-controlled timing precision + +### Memory Flow Optimization + +The TX path maintains frame-level processing until RDK services, with two distinct memory management strategies: + +#### MediaSenderZeroCopyService Path (True Zero-Copy) +``` +Application → Frame Buffer → MediaFrame Wrapper → Direct RDK Transfer → Internal Packetization → Network + ↓ ↓ ↓ ↓ ↓ ↓ +Media Sender GXF Entity Reference Only Zero-Copy Transfer RTP Packets Wire Transmission + (ownership transfer) (RDK Internal) +``` + +#### MediaSenderService Path (Single Copy + Pool) +``` +Application → Frame Buffer → MediaFrame Wrapper → Pool Buffer Copy → Internal Packetization → Network + ↓ ↓ ↓ ↓ ↓ ↓ +Media Sender GXF Entity Reference Only Single memcpy RTP Packets Wire Transmission + (to pool buffer) (RDK Internal) + ↓ + Pool Reuse +``` + +**Memory Architecture Comparison:** + +**Zero-Copy Path (`MediaSenderZeroCopyService`):** +- **Frame Input**: Video frames received from application as GXF VideoBuffer or Tensor +- **Reference Management**: MediaFrame wrapper maintains references to original frame buffer +- **Direct Transfer**: Application's frame buffer ownership transferred directly to RDK (no copying) +- **Frame Lifecycle**: Frames are released/destroyed after RDK processing +- **Maximum Efficiency**: True zero-copy from application to RDK + +**Memory Pool Path (`MediaSenderService`):** +- **Frame Input**: Video frames received from application as GXF VideoBuffer or Tensor +- **Reference Management**: MediaFrame wrapper maintains references to original frame buffer +- **Single Copy**: One memcpy operation from application frame to pre-allocated pool buffer +- **Pool Management**: Pool buffers are reused for subsequent frames +- **Sustained Throughput**: Buffering capability for high-throughput sustained transmission +- **Hardware DMA**: RDK performs direct memory access from pool buffers to NIC hardware + +### Performance Characteristics + +#### MediaSenderZeroCopyService (True Zero-Copy Path) +- **Latency**: Absolute minimal latency - no memory copying overhead +- **Throughput**: Maximum single-stream throughput with zero-copy efficiency +- **Memory Efficiency**: Perfect efficiency - application frame buffer used directly by RDK +- **CPU Usage**: Minimal CPU usage - no memory copying operations +- **Frame Rate**: Optimal for variable frame rates and low-latency applications + +#### MediaSenderService (Memory Pool Path) +- **Latency**: Single memory copy latency (application frame → pool buffer) +- **Throughput**: High sustained throughput with buffering capabilities +- **Memory Efficiency**: One copy operation but optimized through buffer pool reuse +- **CPU Usage**: Minimal additional CPU usage for single memory copy +- **Frame Rate**: Optimal for sustained high frame rate streaming with consistent throughput + +#### Common Characteristics +- **Timing Precision**: Hardware-controlled packet scheduling managed by RDK services +- **Packet Processing**: Zero CPU usage for RTP packetization (handled by RDK + hardware) +- **Scalability**: Multiple transmission flows supported through service instances +- **Hardware Integration**: Direct NIC hardware acceleration for transmission + +### Configuration Integration + +The TX path is configured through YAML files that specify: +- **Network Settings**: Interface addresses, destination IP addresses, ports +- **Video Parameters**: Format, resolution, bit depth, frame rate (for RDK packetization) +- **Memory Regions**: Buffer allocation strategies for RDK internal processing +- **Service Mode Selection**: Choose between zero-copy and memory pool modes + +#### Service Mode Configuration + +**Zero-Copy Mode** (`MediaSenderZeroCopyService`): +```yaml +advanced_network: + interfaces: + - tx: + queues: + - rivermax_tx_settings: + use_internal_memory_pool: false # Enables zero-copy mode +``` + +**Memory Pool Mode** (`MediaSenderService`): +```yaml +advanced_network: + interfaces: + - tx: + queues: + - rivermax_tx_settings: + use_internal_memory_pool: true # Enables memory pool mode +``` + +#### Service Selection Decision Flow + +Understanding when each MediaSender service is selected is crucial for optimizing your application: + +```mermaid +flowchart TD + A["Application Frame
Input"] --> B{{"Configuration
use_internal_memory_pool"}} + + B -->|false| C["MediaSenderZeroCopyService
(True Zero-Copy Path)"] + B -->|true| D["MediaSenderService
(Single Copy + Pool Path)"] + + C --> E["No Internal Memory Pool"] + E --> F["Direct Frame Reference
(custom_pkt_data)"] + F --> G["Zero Memory Copies
Only Ownership Transfer"] + G --> H["Frame Released/Destroyed
After RDK Processing"] + + D --> I["Pre-allocated MediaFramePool"] + I --> J["Single Memory Copy
(Frame → Pool Buffer)"] + J --> K["Pool Buffer Reused
After RDK Processing"] + + H --> L["RDK MediaSenderApp
Processing"] + K --> L + L --> M["RTP Packetization
& Network Transmission"] + + %% Usage Scenarios + N["Usage Scenarios"] --> O["Pipeline Mode
(Zero-Copy)"] + N --> P["Data Generation Mode
(Memory Pool)"] + + O --> Q["• Operators receiving MediaFrame/VideoBuffer/Tensor
• Pipeline processing
• Frame-to-frame transformations
• Real-time streaming applications"] + + P --> R["• File readers
• Camera/sensor operators
• Synthetic data generators
• Batch processing applications"] + + %% Connect scenarios to services + Q -.-> C + R -.-> D + + %% Styling + classDef configNode fill:#f9f9f9,stroke:#333,stroke-width:2px + classDef zeroCopyPath fill:#e8f5e8,stroke:#4caf50,stroke-width:3px + classDef poolPath fill:#fff8e1,stroke:#ff9800,stroke-width:3px + classDef rdkLayer fill:#e3f2fd + classDef usageLayer fill:#f3e5f5 + + class A,B configNode + class C,E,F,G,H zeroCopyPath + class D,I,J,K poolPath + class L,M rdkLayer + class N,O,P,Q,R usageLayer +``` + +#### When to Use Each Mode + +**Use Zero-Copy Mode** (`use_internal_memory_pool: false`): +- **Low-latency applications**: When minimal latency is critical +- **Variable frame rates**: When frame timing is irregular +- **Memory-constrained environments**: When minimizing memory usage is important +- **Single/few streams**: When not requiring high sustained throughput buffering + +**Use Memory Pool Mode** (`use_internal_memory_pool: true`): +- **High sustained throughput**: When streaming at consistent high frame rates +- **Buffering requirements**: When you need frame buffering capabilities +- **Multiple concurrent streams**: When handling multiple transmission flows +- **Production broadcast**: When requiring consistent performance under sustained load + +This TX architecture provides professional-grade media transmission by maintaining frame-level processing in Holohub components while delegating all packet-level operations to optimized RDK services. The key advantages are: +- **MediaSenderZeroCopyService**: True zero-copy frame handoff for maximum efficiency and minimal latency +- **MediaSenderService**: Single copy with memory pool management for sustained high-throughput transmission +Both modes benefit from hardware-accelerated packet processing in the RDK layer. + +## System Requirements + +> [!IMPORTANT] +> Review the [High Performance Networking tutorial](../../tutorials/high_performance_networking/README.md) for guided instructions to configure your system and test the Advanced Network Media operators. + +- Linux +- NVIDIA NIC with ConnectX-6 or later chip +- NVIDIA Rivermax SDK +- System tuning as described in the High Performance Networking tutorial +- Sufficient memory and bandwidth for media streaming workloads + diff --git a/operators/advanced_network_media/advanced_network_media_rx/CMakeLists.txt b/operators/advanced_network_media/advanced_network_media_rx/CMakeLists.txt new file mode 100644 index 0000000000..26632b3892 --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_rx/CMakeLists.txt @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +cmake_minimum_required(VERSION 3.20) +project(advanced_network_media_rx) + +find_package(holoscan 2.6 REQUIRED CONFIG + PATHS "/opt/nvidia/holoscan" "/workspace/holoscan-sdk/install") + +add_library(${PROJECT_NAME} SHARED + adv_network_media_rx.cpp + frame_assembly_controller.cpp + memory_copy_strategies.cpp + media_frame_assembler.cpp + network_burst_processor.cpp +) + +add_library(holoscan::ops::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) + +target_include_directories(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../../advanced_network + ${CMAKE_CURRENT_SOURCE_DIR}/../../advanced_network/advanced_network +) + +target_link_libraries(${PROJECT_NAME} + PRIVATE + holoscan::core + GXF::multimedia + rivermax-dev-kit + advanced_network_common + advanced_network_media_common +) + +# Add CUDA support for memory copy strategies +find_package(CUDAToolkit REQUIRED) +target_link_libraries(${PROJECT_NAME} + PRIVATE + CUDA::cudart +) + +set_target_properties(${PROJECT_NAME} PROPERTIES + OUTPUT_NAME "holoscan_op_advanced_network_media_rx" + EXPORT_NAME ops::advanced_network_media_rx +) + +# Installation +install( + TARGETS + ${PROJECT_NAME} + EXPORT holoscan-networking-targets + COMPONENT advanced_network-cpp +) + +# Install only public interface headers (detail namespace classes are internal) +install( + FILES + frame_provider.h + media_frame_assembler.h + network_burst_processor.h + DESTINATION include/holoscan/operators/advanced_network_media_rx + COMPONENT advanced_network-dev +) + +if(HOLOHUB_BUILD_PYTHON) + add_subdirectory(python) +endif() diff --git a/operators/advanced_network_media/advanced_network_media_rx/adv_network_media_rx.cpp b/operators/advanced_network_media/advanced_network_media_rx/adv_network_media_rx.cpp new file mode 100644 index 0000000000..f375daee36 --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_rx/adv_network_media_rx.cpp @@ -0,0 +1,797 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "adv_network_media_rx.h" +#include "advanced_network/common.h" +#include "../common/adv_network_media_common.h" +#include "media_frame_assembler.h" +#include "network_burst_processor.h" +#include +#include +#include "../common/frame_buffer.h" +#include "../common/video_parameters.h" +#include "advanced_network/managers/rivermax/rivermax_ano_data_types.h" + +namespace holoscan::ops { + +namespace ano = holoscan::advanced_network; +using holoscan::advanced_network::AnoBurstExtendedInfo; +using holoscan::advanced_network::BurstParams; +using holoscan::advanced_network::Status; + +constexpr size_t FRAMES_IN_POOL = 250; +constexpr size_t PACKETS_DISPLAY_INTERVAL = 1000000; // 1e6 packets + +#if ENABLE_STATISTICS_LOGGING +// Statistics timing constants +constexpr size_t STATS_REPORT_INTERVAL_MS = 30000; // Report every 30 seconds +constexpr size_t STATS_WINDOW_SIZE_MS = 30000; // Calculate rates over 30 seconds + +// Constraint: Window must be at least as large as report interval to have enough samples +static_assert(STATS_WINDOW_SIZE_MS >= STATS_REPORT_INTERVAL_MS, + "STATS_WINDOW_SIZE_MS must be >= STATS_REPORT_INTERVAL_MS to ensure " + "sufficient samples for rate calculation"); + +// Structure to hold timestamped statistics samples +struct StatsSample { + std::chrono::steady_clock::time_point timestamp; + size_t packets_received; + size_t bursts_processed; + size_t frames_emitted; +}; +#endif + +// Enumeration for output format types +enum class OutputFormatType { VIDEO_BUFFER, TENSOR }; + +/** + * @brief Frame completion handler for the RX operator + */ +class RxOperatorFrameCompletionHandler : public IFrameCompletionHandler { + public: + explicit RxOperatorFrameCompletionHandler(class AdvNetworkMediaRxOpImpl* impl) : impl_(impl) {} + + void on_frame_completed(std::shared_ptr frame) override; + void on_frame_error(const std::string& error_message) override; + + private: + class AdvNetworkMediaRxOpImpl* impl_; +}; + +/** + * @class AdvNetworkMediaRxOpImpl + * @brief Implementation class for the AdvNetworkMediaRxOp operator. + * + * Handles high-level network management, frame pool management, and + * coordinates with MediaFrameAssembler for packet-level operations. + */ +class AdvNetworkMediaRxOpImpl : public IFrameProvider { + public: + /** + * @brief Constructs an implementation for the given operator. + * + * @param parent Reference to the parent operator. + */ + explicit AdvNetworkMediaRxOpImpl(AdvNetworkMediaRxOp& parent) : parent_(parent) {} + + /** + * @brief Initializes the implementation. + * + * Sets up the network port, allocates frame buffers, and prepares + * for media reception. + */ + void initialize() { + ANM_LOG_INFO("AdvNetworkMediaRxOp::initialize()"); + + // Initialize timing for statistics +#if ENABLE_STATISTICS_LOGGING + start_time_ = std::chrono::steady_clock::now(); + last_stats_report_ = start_time_; + + // Initialize rolling window with first sample + StatsSample initial_sample{start_time_, 0, 0, 0}; + stats_samples_.push_back(initial_sample); +#endif + + port_id_ = ano::get_port_id(parent_.interface_name_.get()); + if (port_id_ == -1) { + std::string error_message = "Invalid RX port interface name '" + + std::string(parent_.interface_name_.get()) + + "' specified in the config"; + ANM_LOG_ERROR("Invalid RX port {} specified in the config", parent_.interface_name_.get()); + throw std::runtime_error(error_message); + } else { + ANM_CONFIG_LOG("RX port {} found", port_id_); + } + + // Convert params to video parameters + auto video_sampling = get_video_sampling_format(parent_.video_format_.get()); + auto color_bit_depth = get_color_bit_depth(parent_.bit_depth_.get()); + + // Get video format and calculate frame size + video_format_ = get_expected_gxf_video_format(video_sampling, color_bit_depth); + frame_size_ = calculate_frame_size( + parent_.frame_width_.get(), parent_.frame_height_.get(), video_sampling, color_bit_depth); + + // Determine output format type + const auto& output_format_str = parent_.output_format_.get(); + if (output_format_str == "tensor") { + output_format_ = OutputFormatType::TENSOR; + ANM_CONFIG_LOG("Using Tensor output format"); + } else { + output_format_ = OutputFormatType::VIDEO_BUFFER; + ANM_CONFIG_LOG("Using VideoBuffer output format"); + } + + // Determine memory location type for output frames + const auto& memory_location_str = parent_.memory_location_.get(); + if (memory_location_str == "host") { + storage_type_ = nvidia::gxf::MemoryStorageType::kHost; + ANM_CONFIG_LOG("Using Host memory location for output frames"); + } else { + storage_type_ = nvidia::gxf::MemoryStorageType::kDevice; + ANM_CONFIG_LOG("Using Device memory location for output frames"); + } + + // Create pool of allocated frame buffers + create_frame_pool(); + + // Create media frame assembler and network burst processor + create_media_frame_assembler(); + + // Create network burst processor + burst_processor_ = std::make_unique(assembler_); + } + + /** + * @brief Creates a pool of frame buffers for receiving frames. + */ + void create_frame_pool() { + frames_pool_.clear(); + + // Get the appropriate channel count based on video format + uint32_t channels = get_channel_count_for_format(video_format_); + + for (size_t i = 0; i < FRAMES_IN_POOL; ++i) { + void* data = nullptr; + + // Allocate memory based on storage type + if (storage_type_ == nvidia::gxf::MemoryStorageType::kHost) { + CUDA_TRY(cudaMallocHost(&data, frame_size_)); + } else { + CUDA_TRY(cudaMalloc(&data, frame_size_)); + } + + // Create appropriate frame buffer type + if (output_format_ == OutputFormatType::TENSOR) { + frames_pool_.push_back(std::make_shared( + data, + frame_size_, + parent_.frame_width_.get(), + parent_.frame_height_.get(), + channels, // Use format-specific channel count + video_format_, + storage_type_)); + } else { + frames_pool_.push_back( + std::make_shared(data, + frame_size_, + parent_.frame_width_.get(), + parent_.frame_height_.get(), + video_format_, + storage_type_)); + } + } + } + + /** + * @brief Creates the media frame assembler with minimal configuration + * @note Full configuration will be done when first burst arrives + */ + void create_media_frame_assembler() { + // Create minimal assembler configuration (will be completed from burst data) + auto config = AssemblerConfiguration{}; + + // Set operator-known parameters only + config.source_memory_type = + nvidia::gxf::MemoryStorageType::kHost; // Will be updated from burst + config.destination_memory_type = storage_type_; + config.enable_memory_copy_strategy_detection = true; + config.force_contiguous_memory_copy_strategy = false; + + // Create frame provider (this class implements IFrameProvider) + auto frame_provider = std::shared_ptr(this, [](IFrameProvider*) {}); + + // Create frame assembler with minimal config + assembler_ = std::make_shared(frame_provider, config); + + // Create completion handler + completion_handler_ = std::make_shared(this); + assembler_->set_completion_handler(completion_handler_); + + ANM_CONFIG_LOG("Media frame assembler created with minimal configuration"); + } + + /** + * @brief Clean up resources properly when the operator is destroyed. + */ + void cleanup() { + // Free all allocated frames + for (auto& frame : frames_pool_) { + if (storage_type_ == nvidia::gxf::MemoryStorageType::kHost) { + CUDA_TRY(cudaFreeHost(frame->get())); + } else { + CUDA_TRY(cudaFree(frame->get())); + } + } + frames_pool_.clear(); + + // Free any frames in the ready queue + for (auto& frame : ready_frames_) { + if (storage_type_ == nvidia::gxf::MemoryStorageType::kHost) { + CUDA_TRY(cudaFreeHost(frame->get())); + } else { + CUDA_TRY(cudaFree(frame->get())); + } + } + ready_frames_.clear(); + + // Free all in-flight network bursts awaiting cleanup + while (!bursts_awaiting_cleanup_.empty()) { + auto burst_to_free = bursts_awaiting_cleanup_.front(); + ano::free_all_packets_and_burst_rx(burst_to_free); + bursts_awaiting_cleanup_.pop_front(); + } + bursts_awaiting_cleanup_.clear(); + } + + /** + * @brief Processes a received burst of packets and generates video frames. + * + * @param op_input The operator input context. + * @param op_output The operator output context. + * @param context The execution context. + */ + void compute(InputContext& op_input, OutputContext& op_output, ExecutionContext& context) { + compute_calls_++; + + BurstParams* burst; + auto status = ano::get_rx_burst(&burst, port_id_, parent_.queue_id_.get()); + if (status != Status::SUCCESS) + return; + + const auto& packets_received = burst->hdr.hdr.num_pkts; + total_packets_received_ += packets_received; + total_bursts_processed_++; + + if (packets_received == 0) { + ano::free_all_packets_and_burst_rx(burst); + return; + } + + // Report periodic comprehensive statistics + report_periodic_statistics(); + + ANM_FRAME_TRACE("Processing burst: port={}, queue={}, packets={}, burst_ptr={}", + port_id_, + parent_.queue_id_.get(), + packets_received, + static_cast(burst)); + + append_to_frame(burst); + + if (ready_frames_.empty()) + return; + + size_t total_frames = ready_frames_.size(); + + if (total_frames > 1) { + size_t frames_to_drop = total_frames - 1; + total_frames_dropped_ += frames_to_drop; + ANM_LOG_WARN( + "Multiple frames ready ({}), dropping {} earlier frames to prevent pipeline issues", + total_frames, + frames_to_drop); + } + + ANM_FRAME_TRACE("Ready frames count: {}, processing frame emission", total_frames); + + // Pop all frames but keep only the last one + std::shared_ptr last_frame = nullptr; + while (auto frame = pop_ready_frame()) { + if (last_frame) { + // Return the previous frame back to pool (dropping it) + frames_pool_.push_back(last_frame); + ANM_FRAME_TRACE("Dropped frame returned to pool: size={}, ptr={}", + last_frame->get_size(), + static_cast(last_frame->get())); + } + last_frame = frame; + } + + // Emit only the last (most recent) frame + if (last_frame) { + total_frames_emitted_++; + ANM_FRAME_TRACE("Emitting latest frame: {} bytes", last_frame->get_size()); + ANM_FRAME_TRACE("Frame emission details: size={}, ptr={}, memory_location={}", + last_frame->get_size(), + static_cast(last_frame->get()), + static_cast(last_frame->get_memory_location())); + auto result = create_frame_entity(last_frame, context); + op_output.emit(result); + } + } + + /** + * @brief Appends packet data from a burst to the current frame being constructed. + * + * @param burst The burst containing packets to process. + */ + void append_to_frame(BurstParams* burst) { + // Configure assembler on first burst + if (!assembler_configured_) { + configure_assembler_from_burst(burst); + assembler_configured_ = true; + } + + size_t ready_frames_before = ready_frames_.size(); + + ANM_FRAME_TRACE("Processing burst: ready_frames_before={}, queue_size={}, burst_packets={}", + ready_frames_before, + bursts_awaiting_cleanup_.size(), + burst->hdr.hdr.num_pkts); + + burst_processor_->process_burst(burst); + + size_t ready_frames_after = ready_frames_.size(); + + ANM_FRAME_TRACE("Burst processed: ready_frames_after={}, frames_completed={}", + ready_frames_after, + ready_frames_after - ready_frames_before); + + // If new frames were completed, free all accumulated bursts + if (ready_frames_after > ready_frames_before) { + size_t frames_completed = ready_frames_after - ready_frames_before; + ANM_FRAME_TRACE("{} frame(s) completed, freeing {} accumulated bursts", + frames_completed, + bursts_awaiting_cleanup_.size()); + ANM_FRAME_TRACE("Freeing accumulated bursts: count={}", bursts_awaiting_cleanup_.size()); + while (!bursts_awaiting_cleanup_.empty()) { + auto burst_to_free = bursts_awaiting_cleanup_.front(); + ANM_FRAME_TRACE("Freeing burst: ptr={}, packets={}", + static_cast(burst_to_free), + burst_to_free->hdr.hdr.num_pkts); + ano::free_all_packets_and_burst_rx(burst_to_free); + bursts_awaiting_cleanup_.pop_front(); + } + } + + // Add current burst to the queue after freeing previous bursts + bursts_awaiting_cleanup_.push_back(burst); + + ANM_FRAME_TRACE("Final queue_size={}, burst_ptr={}", + bursts_awaiting_cleanup_.size(), + static_cast(burst)); + } + + /** + * @brief Retrieves a ready frame from the queue if available. + * + * @return Shared pointer to a ready frame, or nullptr if none is available. + */ + std::shared_ptr pop_ready_frame() { + if (ready_frames_.empty()) { + return nullptr; + } + + auto frame = ready_frames_.front(); + ready_frames_.pop_front(); + return frame; + } + + /** + * @brief Creates a GXF entity containing the frame for output. + * + * @param frame The frame to wrap. + * @param context The execution context. + * @return The GXF entity ready for emission. + */ + holoscan::gxf::Entity create_frame_entity(std::shared_ptr frame, + ExecutionContext& context) { + // Create lambda to return frame to pool when done + auto release_func = [this, frame](void*) -> nvidia::gxf::Expected { + frames_pool_.push_back(frame); + return {}; + }; + + if (output_format_ == OutputFormatType::TENSOR) { + // Cast to AllocatedTensorFrameBuffer and wrap + auto tensor_frame = std::static_pointer_cast(frame); + auto entity = tensor_frame->wrap_in_entity(context.context(), release_func); + return holoscan::gxf::Entity(std::move(entity)); + } else { + // Cast to AllocatedVideoBufferFrameBuffer and wrap + auto video_frame = std::static_pointer_cast(frame); + auto entity = video_frame->wrap_in_entity(context.context(), release_func); + return holoscan::gxf::Entity(std::move(entity)); + } + } + + // Frame management methods (used internally) + std::shared_ptr get_allocated_frame() { + if (frames_pool_.empty()) { + throw std::runtime_error("Running out of resources, frames pool is empty"); + } + auto frame = frames_pool_.front(); + frames_pool_.pop_front(); + return frame; + } + + void on_new_frame(std::shared_ptr frame) { + ready_frames_.push_back(frame); + ANM_FRAME_TRACE("New frame ready: {}", frame->get_size()); + } + + std::shared_ptr get_new_frame() override { + return get_allocated_frame(); + } + + size_t get_frame_size() const override { + return frame_size_; + } + + bool has_available_frames() const override { + return !frames_pool_.empty(); + } + + void return_frame_to_pool(std::shared_ptr frame) override { + if (frame) { + frames_pool_.push_back(frame); + ANM_FRAME_TRACE("Frame returned to pool: pool_size={}, frame_ptr={}", + frames_pool_.size(), + static_cast(frame->get())); + } + } + + private: + /** + * @brief Configure assembler with burst parameters and validate against operator parameters + * @param burst The burst containing configuration info + */ + void configure_assembler_from_burst(BurstParams* burst) { + // Access burst extended info from custom_burst_data + const auto* burst_info = + reinterpret_cast(&(burst->hdr.custom_burst_data)); + + // Validate operator parameters against burst data + validate_configuration_consistency(burst_info); + + // Configure assembler with burst parameters + assembler_->configure_burst_parameters( + burst_info->header_stride_size, burst_info->payload_stride_size, burst_info->hds_on); + + // Configure memory types based on burst info + nvidia::gxf::MemoryStorageType src_type = burst_info->payload_on_cpu + ? nvidia::gxf::MemoryStorageType::kHost + : nvidia::gxf::MemoryStorageType::kDevice; + + assembler_->configure_memory_types(src_type, storage_type_); + + ANM_CONFIG_LOG( + "Assembler configured from burst: header_stride={}, payload_stride={}, " + "hds_on={}, payload_on_cpu={}, src_memory={}, dst_memory={}", + burst_info->header_stride_size, + burst_info->payload_stride_size, + burst_info->hds_on, + burst_info->payload_on_cpu, + static_cast(src_type), + static_cast(storage_type_)); + } + + /** + * @brief Validate consistency between operator parameters and burst configuration + * @param burst_info The burst configuration data + */ + void validate_configuration_consistency(const AnoBurstExtendedInfo* burst_info) { + // Validate HDS configuration + bool operator_hds = parent_.hds_.get(); + bool burst_hds = burst_info->hds_on; + + if (operator_hds != burst_hds) { + ANM_LOG_WARN( + "HDS configuration mismatch: operator parameter={}, burst data={} - using burst data as " + "authoritative", + operator_hds, + burst_hds); + } + + ANM_CONFIG_LOG("Configuration validation completed: operator_hds={}, burst_hds={}", + operator_hds, + burst_hds); + } + + /** + * @brief Report periodic statistics for monitoring + */ + void report_periodic_statistics() { +#if ENABLE_STATISTICS_LOGGING + auto now = std::chrono::steady_clock::now(); + auto time_since_last_report = + std::chrono::duration_cast(now - last_stats_report_).count(); + + if (time_since_last_report >= STATS_REPORT_INTERVAL_MS) { + // Add current sample to rolling window + StatsSample current_sample{now, + total_packets_received_, + total_bursts_processed_, + total_frames_emitted_}; + stats_samples_.push_back(current_sample); + + // Remove samples outside the window, but always keep at least 2 samples for rate calculation + auto window_start_time = now - std::chrono::milliseconds(STATS_WINDOW_SIZE_MS); + while (stats_samples_.size() > 2 && + stats_samples_.front().timestamp < window_start_time) { + stats_samples_.pop_front(); + } + + // Calculate rates based on rolling window + double window_packets_per_sec = 0.0; + double window_frames_per_sec = 0.0; + double window_bursts_per_sec = 0.0; + double actual_window_duration_sec = 0.0; + + if (stats_samples_.size() >= 2) { + const auto& oldest = stats_samples_.front(); + const auto& newest = stats_samples_.back(); + + auto window_duration_ms = std::chrono::duration_cast( + newest.timestamp - oldest.timestamp).count(); + actual_window_duration_sec = window_duration_ms / 1000.0; + + if (actual_window_duration_sec > 0.0) { + size_t window_packets = newest.packets_received - oldest.packets_received; + size_t window_bursts = newest.bursts_processed - oldest.bursts_processed; + size_t window_frames = newest.frames_emitted - oldest.frames_emitted; + + window_packets_per_sec = window_packets / actual_window_duration_sec; + window_frames_per_sec = window_frames / actual_window_duration_sec; + window_bursts_per_sec = window_bursts / actual_window_duration_sec; + } + } + + // Calculate lifetime averages + auto total_runtime = + std::chrono::duration_cast(now - start_time_).count(); + double lifetime_packets_per_sec = + total_runtime > 0 ? static_cast(total_packets_received_) / total_runtime : 0.0; + double lifetime_frames_per_sec = + total_runtime > 0 ? static_cast(total_frames_emitted_) / total_runtime : 0.0; + double lifetime_bursts_per_sec = + total_runtime > 0 ? static_cast(total_bursts_processed_) / total_runtime : 0.0; + + ANM_STATS_LOG("AdvNetworkMediaRx Statistics Report:"); + ANM_STATS_LOG(" Runtime: {} seconds", total_runtime); + ANM_STATS_LOG(" Total packets received: {}", total_packets_received_); + ANM_STATS_LOG(" Total bursts processed: {}", total_bursts_processed_); + ANM_STATS_LOG(" Total frames emitted: {}", total_frames_emitted_); + ANM_STATS_LOG(" Total frames dropped: {}", total_frames_dropped_); + ANM_STATS_LOG(" Compute calls: {}", compute_calls_); + + // Report current rates with actual measurement window + if (actual_window_duration_sec > 0.0) { + ANM_STATS_LOG( + " Current rates (over {:.1f}s): {:.2f} packets/sec, {:.2f} frames/sec, " + "{:.2f} bursts/sec", + actual_window_duration_sec, + window_packets_per_sec, + window_frames_per_sec, + window_bursts_per_sec); + } else { + ANM_STATS_LOG(" Current rates: N/A (insufficient samples, need {} more seconds)", + STATS_REPORT_INTERVAL_MS / 1000); + } + + ANM_STATS_LOG( + " Lifetime avg rates: {:.2f} packets/sec, {:.2f} frames/sec, " + "{:.2f} bursts/sec", + lifetime_packets_per_sec, + lifetime_frames_per_sec, + lifetime_bursts_per_sec); + ANM_STATS_LOG(" Ready frames queue: {}, Burst cleanup queue: {}", + ready_frames_.size(), + bursts_awaiting_cleanup_.size()); + ANM_STATS_LOG(" Frame pool available: {}", frames_pool_.size()); + + // Report assembler statistics if available + if (assembler_) { + auto assembler_stats = assembler_->get_statistics(); + ANM_STATS_LOG(" Frame Assembler - Packets: {}, Frames completed: {}, Errors recovered: {}", + assembler_stats.packets_processed, + assembler_stats.frames_completed, + assembler_stats.errors_recovered); + ANM_STATS_LOG(" Frame Assembler - Current state: {}, Strategy: {}", + assembler_stats.current_frame_state, + assembler_stats.current_strategy); + } + + last_stats_report_ = now; + } +#endif + } + + private: + AdvNetworkMediaRxOp& parent_; + int port_id_; + + // Frame assembly components + std::shared_ptr assembler_; + std::shared_ptr completion_handler_; + std::unique_ptr burst_processor_; + + // Frame management + std::deque> frames_pool_; + std::deque> ready_frames_; + std::deque bursts_awaiting_cleanup_; + + // Enhanced statistics and configuration + size_t total_packets_received_ = 0; + size_t total_bursts_processed_ = 0; + size_t total_frames_emitted_ = 0; + size_t total_frames_dropped_ = 0; + size_t compute_calls_ = 0; +#if ENABLE_STATISTICS_LOGGING + std::chrono::steady_clock::time_point start_time_; + std::chrono::steady_clock::time_point last_stats_report_; + std::deque stats_samples_; // Rolling window of statistics samples +#endif + + nvidia::gxf::VideoFormat video_format_; + size_t frame_size_; + OutputFormatType output_format_{OutputFormatType::VIDEO_BUFFER}; + nvidia::gxf::MemoryStorageType storage_type_{nvidia::gxf::MemoryStorageType::kDevice}; + bool assembler_configured_ = false; ///< Whether assembler has been configured from burst data +}; + +// ======================================================================================== +// RxOperatorFrameCompletionHandler Implementation +// ======================================================================================== + +void RxOperatorFrameCompletionHandler::on_frame_completed(std::shared_ptr frame) { + if (!impl_ || !frame) + return; + + // Add completed frame to ready queue (same as old on_new_frame) + impl_->on_new_frame(frame); + + ANM_FRAME_TRACE("Frame assembly completed: {} bytes", frame->get_size()); +} + +void RxOperatorFrameCompletionHandler::on_frame_error(const std::string& error_message) { + if (!impl_) + return; + + ANM_LOG_ERROR("Frame assembly error: {}", error_message); + // Could add error statistics or recovery logic here +} + +// ======================================================================================== +// AdvNetworkMediaRxOp Implementation +// ======================================================================================== + +AdvNetworkMediaRxOp::AdvNetworkMediaRxOp() : pimpl_(nullptr) {} + +AdvNetworkMediaRxOp::~AdvNetworkMediaRxOp() { + if (pimpl_) { + pimpl_->cleanup(); // Clean up allocated resources + delete pimpl_; + pimpl_ = nullptr; + } +} + +void AdvNetworkMediaRxOp::setup(OperatorSpec& spec) { + ANM_LOG_INFO("AdvNetworkMediaRxOp::setup() - Configuring operator parameters"); + + spec.output("out_video_buffer"); + spec.param(interface_name_, + "interface_name", + "Name of NIC from advanced_network config", + "Name of NIC from advanced_network config"); + spec.param(queue_id_, "queue_id", "Queue ID", "Queue ID", default_queue_id); + spec.param(frame_width_, "frame_width", "Frame width", "Width of the frame", 1920); + spec.param(frame_height_, "frame_height", "Frame height", "Height of the frame", 1080); + spec.param(bit_depth_, "bit_depth", "Bit depth", "Number of bits per pixel", 8); + spec.param( + video_format_, "video_format", "Video Format", "Video sample format", std::string("RGB888")); + spec.param(hds_, + "hds", + "Header Data Split", + "The packets received split Data in the GPU and Headers in the CPU"); + spec.param(output_format_, + "output_format", + "Output Format", + "Output format type ('video_buffer' or 'tensor')", + std::string("video_buffer")); + spec.param(memory_location_, + "memory_location", + "Memory Location for Output Frames", + "Memory location for output frames ('host' or 'devices')", + std::string("device")); + + ANM_CONFIG_LOG("AdvNetworkMediaRxOp setup completed - parameters registered"); +} + +void AdvNetworkMediaRxOp::initialize() { + ANM_LOG_INFO("AdvNetworkMediaRxOp::initialize() - Starting operator initialization"); + holoscan::Operator::initialize(); + + ANM_CONFIG_LOG("Creating implementation instance for AdvNetworkMediaRxOp"); + if (!pimpl_) { + pimpl_ = new AdvNetworkMediaRxOpImpl(*this); + } + + ANM_CONFIG_LOG( + "Initializing implementation with parameters: interface={}, queue_id={}, frame_size={}x{}, " + "format={}, hds={}, output_format={}, memory={}", + interface_name_.get(), + queue_id_.get(), + frame_width_.get(), + frame_height_.get(), + video_format_.get(), + hds_.get(), + output_format_.get(), + memory_location_.get()); + + pimpl_->initialize(); + + ANM_LOG_INFO("AdvNetworkMediaRxOp initialization completed successfully"); +} + +void AdvNetworkMediaRxOp::compute(InputContext& op_input, OutputContext& op_output, + ExecutionContext& context) { + ANM_FRAME_TRACE("AdvNetworkMediaRxOp::compute() - Processing frame"); + +#if ENABLE_PERFORMANCE_LOGGING + auto start_time = std::chrono::high_resolution_clock::now(); +#endif + + try { + pimpl_->compute(op_input, op_output, context); + +#if ENABLE_PERFORMANCE_LOGGING + auto end_time = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end_time - start_time); + + ANM_PERF_LOG("AdvNetworkMediaRxOp::compute() completed in {} microseconds", duration.count()); +#endif + } catch (const std::exception& e) { + ANM_LOG_ERROR("AdvNetworkMediaRxOp::compute() failed with exception: {}", e.what()); + throw; + } catch (...) { + ANM_LOG_ERROR("AdvNetworkMediaRxOp::compute() failed with unknown exception"); + throw; + } +} + +}; // namespace holoscan::ops diff --git a/operators/advanced_network_media/advanced_network_media_rx/adv_network_media_rx.h b/operators/advanced_network_media/advanced_network_media_rx/adv_network_media_rx.h new file mode 100644 index 0000000000..1a8faa1ebc --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_rx/adv_network_media_rx.h @@ -0,0 +1,76 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OPERATORS_ADVANCED_NETWORK_MEDIA_RX_ADV_NETWORK_MEDIA_RX_H_ +#define OPERATORS_ADVANCED_NETWORK_MEDIA_RX_ADV_NETWORK_MEDIA_RX_H_ + +#include + +namespace holoscan::ops { + +// Forward declare the implementation class +class AdvNetworkMediaRxOpImpl; + +/** + * @class AdvNetworkMediaRxOp + * @brief Operator for receiving media frames over advanced network infrastructure. + * + * This operator receives video frames over Rivermax-enabled network infrastructure + * and outputs them as GXF VideoBuffer entities. + */ +class AdvNetworkMediaRxOp : public Operator { + public: + static constexpr uint16_t default_queue_id = 0; + + HOLOSCAN_OPERATOR_FORWARD_ARGS(AdvNetworkMediaRxOp) + + /** + * @brief Constructs an AdvNetworkMediaRxOp operator. + */ + AdvNetworkMediaRxOp(); + + /** + * @brief Destroys the AdvNetworkMediaRxOp operator and its implementation. + */ + ~AdvNetworkMediaRxOp(); + + void initialize() override; + void setup(OperatorSpec& spec) override; + void compute(InputContext& op_input, OutputContext& op_output, + ExecutionContext& context) override; + + protected: + // Operator parameters + Parameter interface_name_; + Parameter queue_id_; + Parameter frame_width_; + Parameter frame_height_; + Parameter bit_depth_; + Parameter video_format_; + Parameter hds_; + Parameter output_format_; + Parameter memory_location_; + + private: + friend class AdvNetworkMediaRxOpImpl; + + AdvNetworkMediaRxOpImpl* pimpl_; +}; + +} // namespace holoscan::ops + +#endif // OPERATORS_ADVANCED_NETWORK_MEDIA_RX_ADV_NETWORK_MEDIA_RX_H_ diff --git a/operators/advanced_network_media/advanced_network_media_rx/frame_assembly_controller.cpp b/operators/advanced_network_media/advanced_network_media_rx/frame_assembly_controller.cpp new file mode 100644 index 0000000000..f88c693a86 --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_rx/frame_assembly_controller.cpp @@ -0,0 +1,480 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "frame_assembly_controller.h" +#include "memory_copy_strategies.h" +#include "../common/adv_network_media_common.h" + +#include +#include +#include +#include + +namespace holoscan::ops { +namespace detail { + +// Import public interfaces for cleaner usage +using holoscan::ops::IFrameProvider; + +// ======================================================================================== +// FrameAssemblyController Implementation +// ======================================================================================== + +FrameAssemblyController::FrameAssemblyController(std::shared_ptr frame_provider) + : frame_provider_(frame_provider) { + if (!frame_provider_) { + throw std::invalid_argument("FrameProvider cannot be null"); + } + + // Don't allocate frame in constructor - wait for first packet + // This prevents reducing the pool size unnecessarily + context_.current_frame = nullptr; + context_.frame_position = 0; + context_.frame_state = FrameState::IDLE; + + ANM_CONFIG_LOG("FrameAssemblyController initialized"); +} + +StateTransitionResult FrameAssemblyController::process_event(StateEvent event, + const RtpParams* rtp_params, + uint8_t* payload) { + // NOTE: rtp_params and payload are currently unused in state transition logic. + // This assembly controller focuses purely on event-driven state transitions. + // Parameters are kept for API consistency and future extensibility. + (void)rtp_params; // Suppress unused parameter warning + (void)payload; // Suppress unused parameter warning + + packets_processed_++; + + ANM_STATE_TRACE( + "State machine context: frame_position={}, current_frame={}, packets_processed={}", + context_.frame_position, + context_.current_frame ? "allocated" : "null", + packets_processed_); + + StateTransitionResult result; + + // Route to appropriate state handler + ANM_STATE_TRACE("Routing event {} to state handler for state {}", + FrameAssemblyHelper::event_to_string(event), + FrameAssemblyHelper::state_to_string(context_.frame_state)); + + switch (context_.frame_state) { + case FrameState::IDLE: + result = handle_idle_state(event, rtp_params, payload); + break; + case FrameState::RECEIVING_PACKETS: + result = handle_receiving_state(event, rtp_params, payload); + break; + case FrameState::ERROR_RECOVERY: + result = handle_error_recovery_state(event, rtp_params, payload); + break; + default: + ANM_STATE_TRACE("Unhandled state encountered: {}", + FrameAssemblyHelper::state_to_string(context_.frame_state)); + result = create_error_result("Unknown state"); + break; + } + + // Attempt state transition whenever new_frame_state differs from current state + if (result.new_frame_state != context_.frame_state) { + FrameState old_state = context_.frame_state; + ANM_STATE_TRACE( + "Attempting state transition: {} -> {} with result actions: allocate={}, complete={}, " + "emit={}", + FrameAssemblyHelper::state_to_string(old_state), + FrameAssemblyHelper::state_to_string(result.new_frame_state), + result.should_allocate_new_frame, + result.should_complete_frame, + result.should_emit_frame); + + if (transition_to_state(result.new_frame_state)) { + ANM_STATE_TRACE("State transition: {} -> {}", + FrameAssemblyHelper::state_to_string(old_state), + FrameAssemblyHelper::state_to_string(result.new_frame_state)); + } else { + result = create_error_result("Invalid state transition"); + } + } else if (result.success) { + ANM_STATE_TRACE( + "Event processed, staying in state {} with result actions: allocate={}, complete={}, " + "emit={}", + FrameAssemblyHelper::state_to_string(context_.frame_state), + result.should_allocate_new_frame, + result.should_complete_frame, + result.should_emit_frame); + } + + return result; +} + +void FrameAssemblyController::reset() { + context_.frame_state = FrameState::IDLE; + context_.frame_position = 0; + + // Reset memory copy strategy if set + if (strategy_) { + strategy_->reset(); + } + + // Don't allocate frame in reset - wait for first packet + // This prevents reducing the pool size unnecessarily + context_.current_frame = nullptr; + + ANM_CONFIG_LOG("Assembly controller reset to initial state"); +} + +bool FrameAssemblyController::advance_frame_position(size_t bytes) { + if (!validate_frame_bounds(bytes)) { + ANM_LOG_ERROR( + "Frame position advancement would exceed bounds: current={}, bytes={}, frame_size={}", + context_.frame_position, + bytes, + context_.current_frame ? context_.current_frame->get_size() : 0); + return false; + } + + context_.frame_position += bytes; + + ANM_FRAME_TRACE( + "Frame position advanced by {} bytes to position {}", bytes, context_.frame_position); + return true; +} + +void FrameAssemblyController::set_strategy(std::shared_ptr strategy) { + strategy_ = strategy; + + if (strategy_) { + ANM_CONFIG_LOG("Strategy set: {}", + strategy_->get_type() == CopyStrategy::CONTIGUOUS ? "CONTIGUOUS" : "STRIDED"); + } else { + ANM_CONFIG_LOG("Strategy cleared"); + } +} + +bool FrameAssemblyController::allocate_new_frame() { + context_.current_frame = frame_provider_->get_new_frame(); + context_.frame_position = 0; + + if (!context_.current_frame) { + ANM_LOG_ERROR("Frame allocation failed"); + return false; + } + + ANM_FRAME_TRACE("New frame allocated: size={}", context_.current_frame->get_size()); + return true; +} + +void FrameAssemblyController::release_current_frame() { + if (context_.current_frame) { + ANM_FRAME_TRACE("Releasing current frame back to pool: size={}", + context_.current_frame->get_size()); + // Return frame to pool through frame provider + frame_provider_->return_frame_to_pool(context_.current_frame); + context_.current_frame.reset(); + context_.frame_position = 0; + } +} + +bool FrameAssemblyController::validate_frame_bounds(size_t required_bytes) const { + if (!context_.current_frame) { + return false; + } + + return (context_.frame_position + required_bytes <= context_.current_frame->get_size()); +} + +bool FrameAssemblyController::transition_to_state(FrameState new_state) { + if (!FrameAssemblyHelper::is_valid_transition(context_.frame_state, new_state)) { + ANM_LOG_ERROR("Invalid state transition: {} -> {}", + FrameAssemblyHelper::state_to_string(context_.frame_state), + FrameAssemblyHelper::state_to_string(new_state)); + return false; + } + + context_.frame_state = new_state; + return true; +} + +StateTransitionResult FrameAssemblyController::handle_idle_state(StateEvent event, + const RtpParams* rtp_params, + uint8_t* payload) { + ANM_STATE_TRACE("IDLE state handler: processing event {}, current_frame={}", + FrameAssemblyHelper::event_to_string(event), + context_.current_frame ? "allocated" : "null"); + + switch (event) { + case StateEvent::PACKET_ARRIVED: { + // Start receiving packets - allocate frame if we don't have one + auto result = create_success_result(FrameState::RECEIVING_PACKETS); + if (!context_.current_frame) { + result.should_allocate_new_frame = true; + } + return result; + } + + case StateEvent::MARKER_DETECTED: { + // Single packet frame (edge case) - complete atomically + auto result = create_success_result(FrameState::IDLE); + + if (!context_.current_frame) { + // No frame allocated yet - allocate one for this single packet + ANM_STATE_TRACE( + "IDLE single-packet frame: no frame allocated, requesting new frame for single packet"); + result.should_allocate_new_frame = true; + // Don't complete/emit since we just allocated + return result; + } else { + // Frame exists - complete it and allocate new one + ANM_STATE_TRACE( + "IDLE single-packet frame: completing existing frame and requesting new one"); + result.should_complete_frame = true; + result.should_emit_frame = true; + if (frame_provider_->has_available_frames()) { + result.should_allocate_new_frame = true; + } else { + ANM_LOG_WARN("Frame completed but pool is empty - staying in IDLE without new frame"); + } + frames_completed_++; + return result; + } + } + + case StateEvent::CORRUPTION_DETECTED: + return create_success_result(FrameState::ERROR_RECOVERY); + + case StateEvent::STRATEGY_DETECTED: + // This should not happen in IDLE state - memory copy strategy detection requires packets + ANM_LOG_WARN("STRATEGY_DETECTED event received in IDLE state - treating as PACKET_ARRIVED"); + return create_success_result(FrameState::RECEIVING_PACKETS); + + default: + return create_error_result("Unexpected event in IDLE state"); + } +} + +StateTransitionResult FrameAssemblyController::handle_receiving_state(StateEvent event, + const RtpParams* rtp_params, + uint8_t* payload) { + ANM_STATE_TRACE("RECEIVING_PACKETS state handler: processing event {}, frame_position={}", + FrameAssemblyHelper::event_to_string(event), + context_.frame_position); + + switch (event) { + case StateEvent::PACKET_ARRIVED: + // Continue receiving packets + return create_success_result(FrameState::RECEIVING_PACKETS); + + case StateEvent::MARKER_DETECTED: { + // Frame completion triggered - complete atomically + // Only allocate new frame if pool has available frames + ANM_STATE_TRACE("RECEIVING_PACKETS->IDLE: marker detected, completing frame at position {}", + context_.frame_position); + auto result = create_success_result(FrameState::IDLE); + result.should_complete_frame = true; + result.should_emit_frame = true; + if (frame_provider_->has_available_frames()) { + result.should_allocate_new_frame = true; + } else { + ANM_LOG_WARN("Frame completed but pool is empty - staying in IDLE without new frame"); + } + frames_completed_++; + return result; + } + + case StateEvent::COPY_EXECUTED: + // This should not happen - COPY_EXECUTED events are not sent to state machine + ANM_LOG_WARN("COPY_EXECUTED received in RECEIVING_PACKETS state - this indicates dead code"); + return create_success_result(FrameState::RECEIVING_PACKETS); + + case StateEvent::CORRUPTION_DETECTED: + return create_success_result(FrameState::ERROR_RECOVERY); + + case StateEvent::STRATEGY_DETECTED: + // Memory copy strategy detection completed while receiving packets + return create_success_result(FrameState::RECEIVING_PACKETS); + + default: + return create_error_result("Unexpected event in RECEIVING_PACKETS state"); + } +} + +StateTransitionResult FrameAssemblyController::handle_error_recovery_state( + StateEvent event, const RtpParams* rtp_params, uint8_t* payload) { + ANM_STATE_TRACE( + "ERROR_RECOVERY state handler: processing event {}, current_frame={}, error_recoveries={}", + FrameAssemblyHelper::event_to_string(event), + context_.current_frame ? "allocated" : "null", + error_recoveries_); + + switch (event) { + case StateEvent::RECOVERY_MARKER: { + // Recovery marker received - release any corrupted frame and try to start new one + // Release corrupted frame first to free the pool slot, then check availability + if (context_.current_frame) { + ANM_FRAME_TRACE("Releasing corrupted frame during recovery: size={}, ptr={}", + context_.current_frame->get_size(), + static_cast(context_.current_frame->get())); + ANM_STATE_TRACE("ERROR_RECOVERY: releasing corrupted frame before recovery completion"); + release_current_frame(); + } + + // Only exit recovery if we can allocate a new frame + if (frame_provider_->has_available_frames()) { + error_recoveries_++; + ANM_LOG_INFO("Recovery marker (M-bit) received - exiting error recovery"); + ANM_STATE_TRACE( + "ERROR_RECOVERY->IDLE: recovery successful, requesting new frame, total recoveries={}", + error_recoveries_); + auto result = create_success_result(FrameState::IDLE); + result.should_allocate_new_frame = true; + return result; + } else { + // Stay in recovery if no frames available + ANM_LOG_WARN("Recovery marker detected but frame pool is empty - staying in recovery"); + auto result = create_success_result(FrameState::ERROR_RECOVERY); + return result; + } + } + + case StateEvent::PACKET_ARRIVED: { + // Stay in recovery state, waiting for marker - packet discarded + ANM_STATE_TRACE("ERROR_RECOVERY: packet discarded, packets_processed={}", packets_processed_); + auto result = create_success_result(FrameState::ERROR_RECOVERY); + result.should_skip_memory_copy_processing = true; + return result; + } + + case StateEvent::CORRUPTION_DETECTED: + // Additional corruption detected, stay in recovery + return create_success_result(FrameState::ERROR_RECOVERY); + + case StateEvent::MARKER_DETECTED: { + // This should not happen in ERROR_RECOVERY - should be RECOVERY_MARKER instead + ANM_LOG_WARN( + "MARKER_DETECTED received in ERROR_RECOVERY state - treating as RECOVERY_MARKER"); + // Release corrupted frame first to free the pool slot, then check availability + if (context_.current_frame) { + ANM_FRAME_TRACE("Releasing corrupted frame during recovery: size={}, ptr={}", + context_.current_frame->get_size(), + static_cast(context_.current_frame->get())); + ANM_STATE_TRACE("ERROR_RECOVERY: releasing corrupted frame before recovery completion"); + release_current_frame(); + } + + // Only exit recovery if we can allocate a new frame + if (frame_provider_->has_available_frames()) { + error_recoveries_++; + auto result = create_success_result(FrameState::IDLE); + result.should_allocate_new_frame = true; + return result; + } else { + // Stay in recovery if no frames available + ANM_LOG_WARN("Marker detected but frame pool is empty - staying in recovery"); + auto result = create_success_result(FrameState::ERROR_RECOVERY); + return result; + } + } + + default: + return create_error_result("Unexpected event in ERROR_RECOVERY state"); + } +} + +StateTransitionResult FrameAssemblyController::create_success_result(FrameState new_state) { + StateTransitionResult result; + result.success = true; + result.new_frame_state = new_state; + return result; +} + +StateTransitionResult FrameAssemblyController::create_error_result( + const std::string& error_message) { + StateTransitionResult result; + result.success = false; + result.error_message = error_message; + result.new_frame_state = FrameState::ERROR_RECOVERY; + return result; +} + +// ======================================================================================== +// FrameAssemblyHelper Implementation +// ======================================================================================== + +std::string FrameAssemblyHelper::state_to_string(FrameState state) { + switch (state) { + case FrameState::IDLE: + return "IDLE"; + case FrameState::RECEIVING_PACKETS: + return "RECEIVING_PACKETS"; + case FrameState::ERROR_RECOVERY: + return "ERROR_RECOVERY"; + default: + return "UNKNOWN"; + } +} + +std::string FrameAssemblyHelper::event_to_string(StateEvent event) { + switch (event) { + case StateEvent::PACKET_ARRIVED: + return "PACKET_ARRIVED"; + case StateEvent::MARKER_DETECTED: + return "MARKER_DETECTED"; + case StateEvent::COPY_EXECUTED: + return "COPY_EXECUTED"; + case StateEvent::CORRUPTION_DETECTED: + return "CORRUPTION_DETECTED"; + case StateEvent::RECOVERY_MARKER: + return "RECOVERY_MARKER"; + case StateEvent::STRATEGY_DETECTED: + return "STRATEGY_DETECTED"; + case StateEvent::FRAME_COMPLETED: + return "FRAME_COMPLETED"; + default: + return "UNKNOWN"; + } +} + +bool FrameAssemblyHelper::is_valid_transition(FrameState from_state, FrameState to_state) { + // Define valid state transitions + switch (from_state) { + case FrameState::IDLE: + return (to_state == FrameState::RECEIVING_PACKETS || to_state == FrameState::ERROR_RECOVERY); + + case FrameState::RECEIVING_PACKETS: + return (to_state == FrameState::RECEIVING_PACKETS || to_state == FrameState::IDLE || + to_state == FrameState::ERROR_RECOVERY); + + case FrameState::ERROR_RECOVERY: + return (to_state == FrameState::IDLE || to_state == FrameState::ERROR_RECOVERY); + + default: + return false; + } +} + +std::vector FrameAssemblyHelper::get_valid_events(FrameState state) { + switch (state) { + case FrameState::IDLE: + return { + StateEvent::PACKET_ARRIVED, StateEvent::MARKER_DETECTED, StateEvent::CORRUPTION_DETECTED}; + + case FrameState::RECEIVING_PACKETS: + return {StateEvent::PACKET_ARRIVED, + StateEvent::MARKER_DETECTED, + StateEvent::CORRUPTION_DETECTED, + StateEvent::STRATEGY_DETECTED}; + + case FrameState::ERROR_RECOVERY: + return { + StateEvent::RECOVERY_MARKER, StateEvent::PACKET_ARRIVED, StateEvent::CORRUPTION_DETECTED}; + + default: + return {}; + } +} + +} // namespace detail +} // namespace holoscan::ops diff --git a/operators/advanced_network_media/advanced_network_media_rx/frame_assembly_controller.h b/operators/advanced_network_media/advanced_network_media_rx/frame_assembly_controller.h new file mode 100644 index 0000000000..fd838aa580 --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_rx/frame_assembly_controller.h @@ -0,0 +1,288 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef OPERATORS_ADVANCED_NETWORK_MEDIA_RX_FRAME_ASSEMBLY_CONTROLLER_H_ +#define OPERATORS_ADVANCED_NETWORK_MEDIA_RX_FRAME_ASSEMBLY_CONTROLLER_H_ + +#include +#include +#include +#include "frame_provider.h" +#include "advanced_network/common.h" +#include "../common/adv_network_media_common.h" +#include "../common/frame_buffer.h" + +namespace holoscan::ops { + +namespace detail { + +// Import public interfaces for cleaner usage +using holoscan::ops::IFrameProvider; + +// Forward declarations for internal implementation +class IMemoryCopyStrategy; + +/** + * @brief Main frame processing states + */ +enum class FrameState { + IDLE, // No active frame, awaiting first packet + RECEIVING_PACKETS, // Actively receiving and processing packets + ERROR_RECOVERY // Frame corrupted, waiting for recovery marker +}; + +/** + * @brief State machine events that drive transitions + */ +enum class StateEvent { + PACKET_ARRIVED, // Regular packet received + MARKER_DETECTED, // M-bit packet received + COPY_EXECUTED, // Copy operation completed + CORRUPTION_DETECTED, // Frame corruption detected + RECOVERY_MARKER, // M-bit received during error recovery + STRATEGY_DETECTED, // Memory copy strategy detection completed + FRAME_COMPLETED // Frame processing finished +}; + +// Forward declaration - enum defined in memory_copy_strategies.h +enum class CopyStrategy; + +/** + * @brief Stride information for strided copy operations + */ +struct StrideInfo { + size_t stride_size = 0; // Stride between packet payloads + size_t payload_size = 0; // Size of each payload +}; + +/** + * @brief Result of state machine transition + */ +struct StateTransitionResult { + bool success = false; // Whether transition succeeded + FrameState new_frame_state = FrameState::IDLE; // New state after transition + bool should_execute_copy = false; // Whether copy operation should be executed + bool should_complete_frame = false; // Whether frame completion should be triggered + bool should_emit_frame = false; // Whether frame should be emitted + bool should_allocate_new_frame = false; // Whether new frame should be allocated + bool should_skip_memory_copy_processing = + false; // Whether to skip memory copy processing (e.g., during recovery) + std::string error_message; // Error description if success=false +}; + +/** + * @brief Assembly controller context (internal state) + */ +struct FrameAssemblyContext { + FrameState frame_state = FrameState::IDLE; + size_t frame_position = 0; // Current byte position in frame + std::shared_ptr current_frame; // Active frame buffer +}; + +// Forward declaration - interface defined in memory_copy_strategies.h +class IMemoryCopyStrategy; + +/** + * @brief Main frame processing state machine + * + * This class provides centralized state management for the entire packet-to-frame + * conversion process, coordinating between strategies, frame allocation, and + * error handling. + * + * @note Design Principle: This state machine focuses purely on state transitions + * and does not directly process packet data. All methods accept rtp_params + * and payload parameters for API consistency and future extensibility, but + * these parameters are currently unused. Actual packet processing is handled + * by the strategy layer (IMemoryCopyStrategy implementations). + */ +class FrameAssemblyController { + public: + /** + * @brief Constructor + * @param frame_provider Provider for frame allocation + */ + explicit FrameAssemblyController(std::shared_ptr frame_provider); + + /** + * @brief Process state machine event + * @param event Event to process + * @param rtp_params RTP packet parameters (currently unused, reserved for future use) + * @param payload Packet payload (currently unused, reserved for future use) + * @return Transition result with actions to execute + * + * @note This state machine focuses purely on state transitions based on events. + * Packet data processing is handled by the strategy layer. The rtp_params + * and payload parameters are provided for API consistency and future + * extensibility but are not currently used in state transition logic. + */ + StateTransitionResult process_event(StateEvent event, const RtpParams* rtp_params = nullptr, + uint8_t* payload = nullptr); + + /** + * @brief Reset assembly controller to initial state + */ + void reset(); + + /** + * @brief Get current frame state + * @return Current state of the assembly controller + */ + FrameState get_frame_state() const { return context_.frame_state; } + + /** + * @brief Get current frame buffer + * @return Current frame or nullptr + */ + std::shared_ptr get_current_frame() const { return context_.current_frame; } + + /** + * @brief Get current frame position + * @return Current byte position in frame + */ + size_t get_frame_position() const { return context_.frame_position; } + + /** + * @brief Advance frame position (with bounds checking) + * @param bytes Number of bytes to advance + * @return True if advancement succeeded + */ + bool advance_frame_position(size_t bytes); + + /** + * @brief Set strategy (for compatibility with old interface) + * @param strategy Strategy to use (can be nullptr) + */ + void set_strategy(std::shared_ptr strategy); + + /** + * @brief Get currently set strategy + * @return Current strategy or nullptr + */ + std::shared_ptr get_strategy() const { return strategy_; } + + /** + * @brief Allocate new frame for processing + * @return True if allocation succeeded + */ + bool allocate_new_frame(); + + /** + * @brief Release current frame back to the pool + */ + void release_current_frame(); + + private: + /** + * @brief Validate frame bounds for operations + * @param required_bytes Number of bytes that will be written + * @return True if operation is safe + */ + bool validate_frame_bounds(size_t required_bytes) const; + + /** + * @brief Transition to new state with validation + * @param new_state Target state + * @return True if transition is valid + */ + bool transition_to_state(FrameState new_state); + + /** + * @brief Handle IDLE state events + * @param event Event to process + * @param rtp_params RTP parameters + * @param payload Packet payload + * @return Transition result + */ + StateTransitionResult handle_idle_state(StateEvent event, const RtpParams* rtp_params, + uint8_t* payload); + + /** + * @brief Handle RECEIVING_PACKETS state events + * @param event Event to process + * @param rtp_params RTP parameters + * @param payload Packet payload + * @return Transition result + */ + StateTransitionResult handle_receiving_state(StateEvent event, const RtpParams* rtp_params, + uint8_t* payload); + + /** + * @brief Handle ERROR_RECOVERY state events + * @param event Event to process + * @param rtp_params RTP parameters + * @param payload Packet payload + * @return Transition result + */ + StateTransitionResult handle_error_recovery_state(StateEvent event, const RtpParams* rtp_params, + uint8_t* payload); + + /** + * @brief Create successful transition result + * @param new_state New state after transition + * @return Success result + */ + StateTransitionResult create_success_result(FrameState new_state); + + /** + * @brief Create error transition result + * @param error_message Error description + * @return Error result + */ + StateTransitionResult create_error_result(const std::string& error_message); + + private: + // Core components + std::shared_ptr frame_provider_; + std::shared_ptr strategy_; + + // Assembly controller context + FrameAssemblyContext context_; + + // Statistics and debugging + size_t packets_processed_ = 0; + size_t frames_completed_ = 0; + size_t error_recoveries_ = 0; +}; + +/** + * @brief Utility functions for frame assembly operations + */ +class FrameAssemblyHelper { + public: + /** + * @brief Convert state to string for logging + * @param state Frame state + * @return String representation + */ + static std::string state_to_string(FrameState state); + + /** + * @brief Convert event to string for logging + * @param event State event + * @return String representation + */ + static std::string event_to_string(StateEvent event); + + /** + * @brief Check if state transition is valid + * @param from_state Source state + * @param to_state Target state + * @return True if transition is allowed + */ + static bool is_valid_transition(FrameState from_state, FrameState to_state); + + /** + * @brief Get expected events for a given state + * @param state Current state + * @return List of valid events for the state + */ + static std::vector get_valid_events(FrameState state); +}; + +} // namespace detail + +} // namespace holoscan::ops + +#endif // OPERATORS_ADVANCED_NETWORK_MEDIA_RX_FRAME_ASSEMBLY_CONTROLLER_H_ diff --git a/operators/advanced_network_media/advanced_network_media_rx/frame_provider.h b/operators/advanced_network_media/advanced_network_media_rx/frame_provider.h new file mode 100644 index 0000000000..304c727d72 --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_rx/frame_provider.h @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef OPERATORS_ADVANCED_NETWORK_MEDIA_RX_FRAME_PROVIDER_H_ +#define OPERATORS_ADVANCED_NETWORK_MEDIA_RX_FRAME_PROVIDER_H_ + +#include +#include "../common/frame_buffer.h" + +namespace holoscan::ops { + +/** + * @brief Interface for frame buffer allocation and management + * + * This interface abstracts frame buffer allocation, allowing different + * components to provide frame buffers without coupling to specific + * allocation strategies. Implementations typically handle memory pool + * management and frame lifecycle. + */ +class IFrameProvider { + public: + virtual ~IFrameProvider() = default; + + /** + * @brief Get a new frame buffer for processing + * @return Frame buffer or nullptr if allocation failed + */ + virtual std::shared_ptr get_new_frame() = 0; + + /** + * @brief Get expected frame size + * @return Frame size in bytes + */ + virtual size_t get_frame_size() const = 0; + + /** + * @brief Check if frames are available for allocation + * @return True if frames are available, false if pool is empty + */ + virtual bool has_available_frames() const = 0; + + /** + * @brief Return a frame back to the pool + * @param frame Frame to return to pool + */ + virtual void return_frame_to_pool(std::shared_ptr frame) = 0; +}; + +} // namespace holoscan::ops + +#endif // OPERATORS_ADVANCED_NETWORK_MEDIA_RX_FRAME_PROVIDER_H_ diff --git a/operators/advanced_network_media/advanced_network_media_rx/media_frame_assembler.cpp b/operators/advanced_network_media/advanced_network_media_rx/media_frame_assembler.cpp new file mode 100644 index 0000000000..5ec754d026 --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_rx/media_frame_assembler.cpp @@ -0,0 +1,693 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "media_frame_assembler.h" +#include "../common/adv_network_media_common.h" +#include +#include +#include +#include + +namespace holoscan::ops { + +// Import detail namespace classes for convenience +using detail::CopyStrategy; +using detail::FrameAssemblyController; +using detail::FrameState; +using detail::IMemoryCopyStrategy; +using detail::MemoryCopyStrategyDetector; +using detail::StateEvent; +using detail::StrategyFactory; + +// RTP sequence number gap threshold for detecting potential buffer wraparound +constexpr int32_t kRtpSequenceGapWraparoundThreshold = 16384; // 2^14 + +// Helper functions to convert internal types to strings for statistics +std::string convert_strategy_to_string(CopyStrategy internal_strategy) { + switch (internal_strategy) { + case CopyStrategy::CONTIGUOUS: + return "CONTIGUOUS"; + case CopyStrategy::STRIDED: + return "STRIDED"; + default: + return "UNKNOWN"; + } +} + +std::string convert_state_to_string(FrameState internal_state) { + switch (internal_state) { + case FrameState::IDLE: + return "IDLE"; + case FrameState::RECEIVING_PACKETS: + return "RECEIVING_PACKETS"; + + case FrameState::ERROR_RECOVERY: + return "ERROR_RECOVERY"; + default: + return "IDLE"; + } +} + +// ======================================================================================== +// MediaFrameAssembler Implementation +// ======================================================================================== + +MediaFrameAssembler::MediaFrameAssembler(std::shared_ptr frame_provider, + const AssemblerConfiguration& config) + : config_(config) { + // Validate configuration + if (!AssemblerConfigurationHelper::validate_configuration(config_)) { + throw std::invalid_argument("Invalid assembler configuration"); + } + + // Create frame assembly controller + assembly_controller_ = std::make_unique(frame_provider); + + // Create memory copy strategy detector if needed + if (config_.enable_memory_copy_strategy_detection && + !config_.force_contiguous_memory_copy_strategy) { + memory_copy_strategy_detector_ = StrategyFactory::create_detector(); + memory_copy_strategy_detection_active_ = true; + } else if (config_.force_contiguous_memory_copy_strategy) { + // Create contiguous memory copy strategy immediately + current_copy_strategy_ = StrategyFactory::create_contiguous_strategy( + config_.source_memory_type, config_.destination_memory_type); + setup_memory_copy_strategy(std::move(current_copy_strategy_)); + memory_copy_strategy_detection_active_ = false; + } + + ANM_CONFIG_LOG("MediaFrameAssembler initialized: strategy_detection={}, force_contiguous={}", + config_.enable_memory_copy_strategy_detection, + config_.force_contiguous_memory_copy_strategy); +} + +void MediaFrameAssembler::set_completion_handler(std::shared_ptr handler) { + completion_handler_ = handler; +} + +void MediaFrameAssembler::configure_burst_parameters(size_t header_stride_size, + size_t payload_stride_size, bool hds_enabled) { + config_.header_stride_size = header_stride_size; + config_.payload_stride_size = payload_stride_size; + config_.hds_enabled = hds_enabled; + + // Configure memory copy strategy detector if active + if (memory_copy_strategy_detector_) { + memory_copy_strategy_detector_->configure_burst_parameters( + header_stride_size, payload_stride_size, hds_enabled); + } + + ANM_CONFIG_LOG("Burst parameters configured: header_stride={}, payload_stride={}, hds={}", + header_stride_size, + payload_stride_size, + hds_enabled); +} + +void MediaFrameAssembler::configure_memory_types(nvidia::gxf::MemoryStorageType source_type, + nvidia::gxf::MemoryStorageType destination_type) { + config_.source_memory_type = source_type; + config_.destination_memory_type = destination_type; + + ANM_CONFIG_LOG("Memory types configured: source={}, destination={}", + static_cast(source_type), + static_cast(destination_type)); + + // If memory copy strategy is already set up, we may need to recreate it with new memory types + if (current_copy_strategy_ && !memory_copy_strategy_detection_active_) { + CopyStrategy strategy_type = current_copy_strategy_->get_type(); + + if (strategy_type == CopyStrategy::CONTIGUOUS) { + current_copy_strategy_ = + StrategyFactory::create_contiguous_strategy(source_type, destination_type); + } + // Note: For strided memory copy strategy, we would need the stride info, so we'd trigger + // redetection + + setup_memory_copy_strategy(std::move(current_copy_strategy_)); + } +} + +void MediaFrameAssembler::process_incoming_packet(const RtpParams& rtp_params, uint8_t* payload) { + try { + // Update statistics + update_statistics(StateEvent::PACKET_ARRIVED); + update_packet_statistics(rtp_params); + + // Determine appropriate event for this packet + StateEvent event = determine_event(rtp_params, payload); + + // Check current state before processing for recovery completion detection + FrameState previous_state = assembly_controller_->get_frame_state(); + + // Process event through assembly controller + auto result = assembly_controller_->process_event(event, &rtp_params, payload); + + if (!result.success) { + ANM_FRAME_ERROR(statistics_.current_frame_number, + "Assembly controller processing failed: {}", + result.error_message); + handle_error_recovery(result.error_message); + return; + } + + // Log error recovery state changes + if (result.new_frame_state == FrameState::ERROR_RECOVERY) { + ANM_STATE_LOG("Error recovery active - discarding packets until M-bit marker received"); + } else if (previous_state == FrameState::ERROR_RECOVERY && + result.new_frame_state == FrameState::IDLE) { + ANM_STATE_LOG("Error recovery completed successfully - resuming normal frame processing"); + } + + // Execute actions based on assembly controller result + execute_actions(result, rtp_params, payload); + + ANM_PACKET_TRACE("Packet processed successfully: seq={}, event={}, new_state={}", + rtp_params.sequence_number, + static_cast(event), + static_cast(result.new_frame_state)); + + // Special logging for recovery marker processing + if (event == StateEvent::RECOVERY_MARKER) { + ANM_STATE_LOG("RECOVERY_MARKER event processed - should have exited error recovery"); + } + } catch (const std::exception& e) { + std::string error_msg = std::string("Exception in packet processing: ") + e.what(); + ANM_FRAME_ERROR(statistics_.current_frame_number, "{}", error_msg); + handle_error_recovery(error_msg); + } +} + +void MediaFrameAssembler::force_memory_copy_strategy_redetection() { + if (memory_copy_strategy_detector_) { + memory_copy_strategy_detector_->reset(); + memory_copy_strategy_detection_active_ = true; + current_copy_strategy_.reset(); + assembly_controller_->set_strategy(nullptr); + statistics_.memory_copy_strategy_redetections++; + + ANM_CONFIG_LOG("Memory copy strategy redetection forced"); + } +} + +void MediaFrameAssembler::reset() { + assembly_controller_->reset(); + + if (memory_copy_strategy_detector_) { + memory_copy_strategy_detector_->reset(); + memory_copy_strategy_detection_active_ = config_.enable_memory_copy_strategy_detection && + !config_.force_contiguous_memory_copy_strategy; + } + + if (config_.force_contiguous_memory_copy_strategy) { + current_copy_strategy_ = StrategyFactory::create_contiguous_strategy( + config_.source_memory_type, config_.destination_memory_type); + setup_memory_copy_strategy(std::move(current_copy_strategy_)); + } else { + current_copy_strategy_.reset(); + } + + // Reset statistics (keep cumulative counters) + statistics_.current_strategy = "UNKNOWN"; + statistics_.current_frame_state = "IDLE"; + statistics_.last_error.clear(); + + ANM_CONFIG_LOG("Media Frame assembler has been reset to initial state"); +} + +MediaFrameAssembler::Statistics MediaFrameAssembler::get_statistics() const { + // Update current state information + statistics_.current_frame_state = + convert_state_to_string(assembly_controller_->get_frame_state()); + + if (current_copy_strategy_) { + statistics_.current_strategy = convert_strategy_to_string(current_copy_strategy_->get_type()); + } + + return statistics_; +} + +bool MediaFrameAssembler::has_accumulated_data() const { + return current_copy_strategy_ && current_copy_strategy_->has_accumulated_data(); +} + +std::shared_ptr MediaFrameAssembler::get_current_frame() const { + return assembly_controller_->get_current_frame(); +} + +size_t MediaFrameAssembler::get_frame_position() const { + return assembly_controller_->get_frame_position(); +} + +StateEvent MediaFrameAssembler::determine_event(const RtpParams& rtp_params, uint8_t* payload) { + // Check for M-bit marker first + if (rtp_params.m_bit) { + if (assembly_controller_->get_frame_state() == FrameState::ERROR_RECOVERY) { + ANM_STATE_LOG("M-bit detected during error recovery - generating RECOVERY_MARKER event"); + return StateEvent::RECOVERY_MARKER; + } else { + return StateEvent::MARKER_DETECTED; + } + } + + // Validate packet integrity + if (!validate_packet_integrity(rtp_params)) { + return StateEvent::CORRUPTION_DETECTED; + } + + // Check if we're in memory copy strategy detection phase + if (memory_copy_strategy_detection_active_ && memory_copy_strategy_detector_) { + if (memory_copy_strategy_detector_->collect_packet( + rtp_params, payload, rtp_params.payload_size)) { + // Enough packets collected, attempt memory copy strategy detection + auto detected_strategy = memory_copy_strategy_detector_->detect_strategy( + config_.source_memory_type, config_.destination_memory_type); + + if (detected_strategy) { + setup_memory_copy_strategy(std::move(detected_strategy)); + memory_copy_strategy_detection_active_ = false; + return StateEvent::STRATEGY_DETECTED; + } else { + // Detection failed, will retry with more packets + return StateEvent::PACKET_ARRIVED; + } + } else { + // Still collecting packets for detection + return StateEvent::PACKET_ARRIVED; + } + } + + return StateEvent::PACKET_ARRIVED; +} + +void MediaFrameAssembler::execute_actions(const StateTransitionResult& result, + const RtpParams& rtp_params, uint8_t* payload) { + ANM_FRAME_TRACE("execute_actions: should_emit_frame={}, should_complete_frame={}, new_state={}", + result.should_emit_frame, + result.should_complete_frame, + static_cast(result.new_frame_state)); + // Memory copy strategy processing (skip during error recovery as indicated by state machine) + if (result.new_frame_state == FrameState::RECEIVING_PACKETS && + !result.should_skip_memory_copy_processing && current_copy_strategy_ && payload) { + StateEvent copy_strategy_result = current_copy_strategy_->process_packet( + *assembly_controller_, payload, rtp_params.payload_size); + + if (copy_strategy_result == StateEvent::CORRUPTION_DETECTED) { + handle_error_recovery("Memory copy strategy detected corruption"); + return; + } else if (copy_strategy_result == StateEvent::COPY_EXECUTED) { + ANM_MEMCOPY_TRACE("Memory copy strategy executed operation successfully"); + } + } + + // Execute pending copies if requested + if (result.should_execute_copy && current_copy_strategy_) { + if (current_copy_strategy_->has_accumulated_data()) { + StateEvent copy_result = + current_copy_strategy_->execute_accumulated_copy(*assembly_controller_); + if (copy_result == StateEvent::CORRUPTION_DETECTED) { + handle_error_recovery("Copy execution failed"); + return; + } + } + } + + // Handle frame completion + if (result.should_complete_frame) { + handle_frame_completion(); + } + + // Handle frame emission + if (result.should_emit_frame) { + auto frame = assembly_controller_->get_current_frame(); + if (frame && completion_handler_) { + ANM_FRAME_TRACE("Emitting frame to completion handler"); + completion_handler_->on_frame_completed(frame); + // Note: frames_completed is incremented in state controller atomic operation + } + } + + // Handle new frame allocation (atomic with frame completion) + if (result.should_allocate_new_frame) { + ANM_FRAME_TRACE("Allocating new frame for next packet sequence"); + if (!assembly_controller_->allocate_new_frame()) { + ANM_FRAME_ERROR(statistics_.current_frame_number, + "Failed to allocate new frame after completion"); + } else { + // Starting a new frame - update statistics + ANM_STATS_UPDATE(statistics_.current_frame_number++; statistics_.frames_started++; + statistics_.packets_in_current_frame = 0; + statistics_.bytes_in_current_frame = 0; + statistics_.first_sequence_in_frame = 0;); + + ANM_STATS_TRACE("Starting new frame {}", statistics_.current_frame_number); + } + } +} + +bool MediaFrameAssembler::handle_memory_copy_strategy_detection(const RtpParams& rtp_params, + uint8_t* payload) { + if (!memory_copy_strategy_detection_active_ || !memory_copy_strategy_detector_) { + return true; // No detection needed or memory copy strategy already available + } + + // Collect packet for analysis + if (memory_copy_strategy_detector_->collect_packet( + rtp_params, payload, rtp_params.payload_size)) { + // Attempt memory copy strategy detection + auto detected_strategy = memory_copy_strategy_detector_->detect_strategy( + config_.source_memory_type, config_.destination_memory_type); + + if (detected_strategy) { + setup_memory_copy_strategy(std::move(detected_strategy)); + memory_copy_strategy_detection_active_ = false; + return true; + } else { + ANM_STRATEGY_LOG("Strategy detection failed, will retry"); + return false; + } + } + + ANM_STRATEGY_LOG("Still collecting packets for strategy detection ({}/{})", + memory_copy_strategy_detector_->get_packets_analyzed(), + MemoryCopyStrategyDetector::DETECTION_PACKET_COUNT); + return false; +} + +void MediaFrameAssembler::setup_memory_copy_strategy( + std::unique_ptr strategy) { + current_copy_strategy_ = std::move(strategy); + + // Note: For the old interface compatibility, we would set the memory copy strategy in the + // assembly controller but since IMemoryCopyStrategy is different from IPacketCopyStrategy, we + // manage it here + + if (current_copy_strategy_) { + ANM_CONFIG_LOG( + "Memory copy strategy setup completed: {}", + current_copy_strategy_->get_type() == CopyStrategy::CONTIGUOUS ? "CONTIGUOUS" : "STRIDED"); + } +} + +bool MediaFrameAssembler::validate_packet_integrity(const RtpParams& rtp_params) { + auto frame = assembly_controller_->get_current_frame(); + if (!frame) { + return false; + } + + int64_t bytes_left = frame->get_size() - assembly_controller_->get_frame_position(); + + if (bytes_left < 0) { + return false; // Frame overflow + } + + bool frame_full = (bytes_left == 0); + if (frame_full && !rtp_params.m_bit) { + return false; // Frame full but no marker + } + + return true; +} + +void MediaFrameAssembler::handle_frame_completion() { + // Execute any accumulated copy operations + if (current_copy_strategy_ && current_copy_strategy_->has_accumulated_data()) { + StateEvent copy_result = + current_copy_strategy_->execute_accumulated_copy(*assembly_controller_); + if (copy_result == StateEvent::CORRUPTION_DETECTED) { + handle_error_recovery("Final copy operation failed"); + return; + } + } + + // Update frame completion statistics + ANM_STATS_UPDATE(statistics_.frames_completed++; statistics_.frames_completed_successfully++; + statistics_.last_frame_completion_time = std::chrono::steady_clock::now();); + + ANM_STATS_TRACE("Frame {} completed successfully - {} packets, {} bytes", + statistics_.current_frame_number, + statistics_.packets_in_current_frame, + statistics_.bytes_in_current_frame); + + // Reset current frame statistics for next frame + ANM_STATS_UPDATE(statistics_.packets_in_current_frame = 0; statistics_.bytes_in_current_frame = 0; + statistics_.first_sequence_in_frame = 0;); +} + +void MediaFrameAssembler::handle_error_recovery(const std::string& error_message) { + ANM_STATS_UPDATE(statistics_.last_error = error_message; statistics_.errors_recovered++; + statistics_.error_recovery_cycles++; + statistics_.frames_dropped++; + statistics_.last_error_time = std::chrono::steady_clock::now();); + + // Log dropped frame information + ANM_FRAME_WARN( + statistics_.current_frame_number, + "Error recovery initiated: {} - discarding {} packets, {} bytes - waiting for M-bit marker", + error_message, + statistics_.packets_in_current_frame, + statistics_.bytes_in_current_frame); + + if (completion_handler_) { + completion_handler_->on_frame_error(error_message); + } + + // Reset memory copy strategy if needed + if (current_copy_strategy_) { + current_copy_strategy_->reset(); + } + + // Reset current frame statistics since frame is being dropped + ANM_STATS_UPDATE(statistics_.packets_in_current_frame = 0; statistics_.bytes_in_current_frame = 0; + statistics_.first_sequence_in_frame = 0;); +} + +void MediaFrameAssembler::update_statistics(StateEvent event) { + switch (event) { + case StateEvent::PACKET_ARRIVED: + statistics_.packets_processed++; + ANM_STATS_UPDATE(statistics_.packets_in_current_frame++); + break; + case StateEvent::STRATEGY_DETECTED: + ANM_STATS_UPDATE(statistics_.memory_copy_strategy_redetections++); + break; + case StateEvent::MARKER_DETECTED: + // Frame completion handled elsewhere + break; + case StateEvent::RECOVERY_MARKER: + // Recovery completion handled elsewhere + break; + case StateEvent::CORRUPTION_DETECTED: + ANM_STATS_UPDATE(statistics_.memory_corruption_errors++); + break; + default: + break; + } +} + +void MediaFrameAssembler::update_packet_statistics(const RtpParams& rtp_params) { + ANM_STATS_UPDATE( + // Check for sequence discontinuity (only if we have a previous sequence number) + if (statistics_.last_sequence_number != 0 && statistics_.packets_processed > 1) { + uint32_t expected_seq = statistics_.last_sequence_number + 1; + if (rtp_params.sequence_number != expected_seq) { + statistics_.sequence_discontinuities++; + + // Check for potential buffer overflow (large gaps) + int32_t gap = + static_cast(rtp_params.sequence_number) - static_cast(expected_seq); + + // Power-of-2 check for buffer wraparound (524288 = 2^19) + if (gap > kRtpSequenceGapWraparoundThreshold && (gap & (gap - 1)) == 0) { + statistics_.buffer_overflow_errors++; + ANM_FRAME_TRACE( + "Frame {}: Potential RX buffer wraparound detected: RTP sequence gap {} " + "(2^{}) - processing pipeline too slow, cannot keep up with incoming data rate", + statistics_.current_frame_number, + gap, + __builtin_ctz(gap)); + } else { + ANM_FRAME_TRACE( + "Frame {}: RTP sequence discontinuity detected: expected {}, got {} (gap of {})", + statistics_.current_frame_number, + expected_seq, + rtp_params.sequence_number, + gap); + } + } + } + + // Update sequence tracking + statistics_.last_sequence_number = rtp_params.sequence_number; + + // Set first sequence in frame if this is the first packet + if (statistics_.first_sequence_in_frame == 0) { + statistics_.first_sequence_in_frame = rtp_params.sequence_number; + } + + // Update byte count + statistics_.bytes_in_current_frame += rtp_params.payload_size;); +} + +// ======================================================================================== +// MediaFrameAssembler Statistics Implementation +// ======================================================================================== + +std::string MediaFrameAssembler::get_statistics_summary() const { +#if ENABLE_STATISTICS_LOGGING + std::ostringstream ss; + auto stats = get_statistics(); + + ss << "MediaFrameAssembler Statistics Summary:\n"; + ss << "========================================\n"; + + // Basic counters + ss << "Basic Counters:\n"; + ss << " Packets processed: " << stats.packets_processed << "\n"; + ss << " Frames completed: " << stats.frames_completed << "\n"; + ss << " Errors recovered: " << stats.errors_recovered << "\n"; + ss << " Strategy redetections: " << stats.memory_copy_strategy_redetections << "\n"; + + // Enhanced frame tracking + ss << "\nFrame Tracking:\n"; + ss << " Current frame number: " << stats.current_frame_number << "\n"; + ss << " Frames started: " << stats.frames_started << "\n"; + ss << " Frames dropped: " << stats.frames_dropped << "\n"; + ss << " Frames completed successfully: " << stats.frames_completed_successfully << "\n"; + if (stats.frames_started > 0) { + double completion_rate = + (double)stats.frames_completed_successfully / stats.frames_started * 100.0; + ss << " Frame completion rate: " << std::fixed << std::setprecision(2) << completion_rate + << "%\n"; + } + + // Enhanced error tracking + ss << "\nError Tracking:\n"; + ss << " Sequence discontinuities: " << stats.sequence_discontinuities << "\n"; + ss << " Buffer overflow errors: " << stats.buffer_overflow_errors << "\n"; + ss << " Memory corruption errors: " << stats.memory_corruption_errors << "\n"; + ss << " Error recovery cycles: " << stats.error_recovery_cycles << "\n"; + + // Current frame metrics + ss << "\nCurrent Frame:\n"; + ss << " Packets in current frame: " << stats.packets_in_current_frame << "\n"; + ss << " Bytes in current frame: " << stats.bytes_in_current_frame << "\n"; + ss << " Last sequence number: " << stats.last_sequence_number << "\n"; + ss << " First sequence in frame: " << stats.first_sequence_in_frame << "\n"; + + // State information + ss << "\nState Information:\n"; + ss << " Current strategy: " << stats.current_strategy << "\n"; + ss << " Current frame state: " << stats.current_frame_state << "\n"; + if (!stats.last_error.empty()) { + ss << " Last error: " << stats.last_error << "\n"; + } + + // Timing information + auto now = std::chrono::steady_clock::now(); + if (stats.last_frame_completion_time != std::chrono::steady_clock::time_point{}) { + auto time_since_last_frame = std::chrono::duration_cast( + now - stats.last_frame_completion_time) + .count(); + ss << "\nTiming:\n"; + ss << " Time since last frame completion: " << time_since_last_frame << " ms\n"; + } + + if (stats.last_error_time != std::chrono::steady_clock::time_point{}) { + auto time_since_last_error = + std::chrono::duration_cast(now - stats.last_error_time).count(); + ss << " Time since last error: " << time_since_last_error << " ms\n"; + } + + return ss.str(); +#else + return "Enhanced statistics disabled for performance (compile with ENABLE_STATISTICS_LOGGING)"; +#endif +} + +// ======================================================================================== +// DefaultFrameCompletionHandler Implementation +// ======================================================================================== + +DefaultFrameCompletionHandler::DefaultFrameCompletionHandler( + std::function)> frame_ready_callback, + std::function error_callback) + : frame_ready_callback_(frame_ready_callback), error_callback_(error_callback) {} + +void DefaultFrameCompletionHandler::on_frame_completed(std::shared_ptr frame) { + if (frame_ready_callback_) { + frame_ready_callback_(frame); + } +} + +void DefaultFrameCompletionHandler::on_frame_error(const std::string& error_message) { + if (error_callback_) { + error_callback_(error_message); + } else { + ANM_LOG_ERROR("Frame processing error: {}", error_message); + } +} + +// ======================================================================================== +// AssemblerConfigurationHelper Implementation +// ======================================================================================== + +AssemblerConfiguration AssemblerConfigurationHelper::create_with_burst_parameters( + size_t header_stride, size_t payload_stride, bool hds_enabled, bool payload_on_cpu, + bool frames_on_host) { + AssemblerConfiguration config; + + config.header_stride_size = header_stride; + config.payload_stride_size = payload_stride; + config.hds_enabled = hds_enabled; + + config.source_memory_type = payload_on_cpu ? nvidia::gxf::MemoryStorageType::kHost + : nvidia::gxf::MemoryStorageType::kDevice; + + config.destination_memory_type = frames_on_host ? nvidia::gxf::MemoryStorageType::kHost + : nvidia::gxf::MemoryStorageType::kDevice; + + config.enable_memory_copy_strategy_detection = true; + config.force_contiguous_memory_copy_strategy = false; + + return config; +} + +AssemblerConfiguration AssemblerConfigurationHelper::create_test_config(bool force_contiguous) { + AssemblerConfiguration config; + + config.source_memory_type = nvidia::gxf::MemoryStorageType::kHost; + config.destination_memory_type = nvidia::gxf::MemoryStorageType::kHost; + config.hds_enabled = false; + config.header_stride_size = 1500; + config.payload_stride_size = 1500; + config.force_contiguous_memory_copy_strategy = force_contiguous; + config.enable_memory_copy_strategy_detection = !force_contiguous; + + return config; +} + +bool AssemblerConfigurationHelper::validate_configuration(const AssemblerConfiguration& config) { + // Basic validation + if (config.enable_memory_copy_strategy_detection && + config.force_contiguous_memory_copy_strategy) { + ANM_LOG_ERROR( + "Configuration error: Cannot enable memory copy strategy detection and force contiguous " + "strategy " + "simultaneously"); + return false; + } + + // Stride validation (if detection is enabled) + if (config.enable_memory_copy_strategy_detection) { + if (config.header_stride_size == 0 && config.payload_stride_size == 0) { + ANM_LOG_WARN( + "Zero stride sizes with strategy detection enabled may affect detection accuracy"); + } + } + + return true; +} + +} // namespace holoscan::ops diff --git a/operators/advanced_network_media/advanced_network_media_rx/media_frame_assembler.h b/operators/advanced_network_media/advanced_network_media_rx/media_frame_assembler.h new file mode 100644 index 0000000000..14737bbe43 --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_rx/media_frame_assembler.h @@ -0,0 +1,348 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef OPERATORS_ADVANCED_NETWORK_MEDIA_RX_MEDIA_FRAME_ASSEMBLER_H_ +#define OPERATORS_ADVANCED_NETWORK_MEDIA_RX_MEDIA_FRAME_ASSEMBLER_H_ + +#include +#include +#include +#include +#include "frame_provider.h" +#include "frame_assembly_controller.h" +#include "memory_copy_strategies.h" +#include "advanced_network/common.h" +#include "../common/frame_buffer.h" + +namespace holoscan::ops { + +// Forward declarations for detail namespace types used internally +namespace detail { +enum class StateEvent; +struct StateTransitionResult; +class FrameAssemblyController; +class MemoryCopyStrategyDetector; +class IMemoryCopyStrategy; +} // namespace detail + +// Import detail types for cleaner private method signatures +using detail::CopyStrategy; +using detail::FrameAssemblyController; +using detail::IMemoryCopyStrategy; +using detail::MemoryCopyStrategyDetector; +using detail::StateEvent; +using detail::StateTransitionResult; + +/** + * @brief Configuration for memory copy strategy detection and memory settings + */ +struct AssemblerConfiguration { + // Memory configuration + nvidia::gxf::MemoryStorageType source_memory_type = nvidia::gxf::MemoryStorageType::kDevice; + nvidia::gxf::MemoryStorageType destination_memory_type = nvidia::gxf::MemoryStorageType::kDevice; + + // Burst configuration + size_t header_stride_size = 0; + size_t payload_stride_size = 0; + bool hds_enabled = false; + + // Detection configuration + bool force_contiguous_memory_copy_strategy = false; + bool enable_memory_copy_strategy_detection = true; +}; + +/** + * @brief Callback interface for frame completion events + */ +class IFrameCompletionHandler { + public: + virtual ~IFrameCompletionHandler() = default; + + /** + * @brief Called when a frame is completed and ready for emission + * @param frame Completed frame buffer + */ + virtual void on_frame_completed(std::shared_ptr frame) = 0; + + /** + * @brief Called when frame processing encounters an error + * @param error_message Error description + */ + virtual void on_frame_error(const std::string& error_message) = 0; +}; + +/** + * @brief Frame assembler for converting packets to frames + * + * This class provides a clean, assembly controller driven approach to converting + * network packets into video frames with automatic memory copy strategy detection and + * robust error handling. + * + * @note Architecture: This class coordinates between three main components: + * - FrameAssemblyController: Assembly controller for state transitions + * - IMemoryCopyStrategy: Strategy pattern for packet data processing + * - MemoryCopyStrategyDetector: Automatic detection of optimal copy strategies + * + * The assembly controller layer focuses purely on state management and does + * not directly process packet data, maintaining clean separation of concerns. + */ +class MediaFrameAssembler { + public: + /** + * @brief Constructor + * @param frame_provider Provider for frame allocation + * @param config Assembler configuration + */ + MediaFrameAssembler(std::shared_ptr frame_provider, + const AssemblerConfiguration& config = {}); + + /** + * @brief Set frame completion handler + * @param handler Callback handler for frame events + */ + void set_completion_handler(std::shared_ptr handler); + + /** + * @brief Configure burst parameters for memory copy strategy detection + * @param header_stride_size Header stride from burst info + * @param payload_stride_size Payload stride from burst info + * @param hds_enabled Whether header data split is enabled + */ + void configure_burst_parameters(size_t header_stride_size, size_t payload_stride_size, + bool hds_enabled); + + /** + * @brief Update memory configuration + * @param source_type Source memory storage type + * @param destination_type Destination memory storage type + */ + void configure_memory_types(nvidia::gxf::MemoryStorageType source_type, + nvidia::gxf::MemoryStorageType destination_type); + + /** + * @brief Process incoming RTP packet - MAIN ENTRY POINT + * @param rtp_params Parsed RTP parameters + * @param payload Packet payload data + */ + void process_incoming_packet(const RtpParams& rtp_params, uint8_t* payload); + + /** + * @brief Force memory copy strategy redetection (for testing or config changes) + */ + void force_memory_copy_strategy_redetection(); + + /** + * @brief Reset Media Frame Assembler to initial state + */ + void reset(); + + /** + * @brief Get current Media Frame Assembler statistics + * @return Statistics structure + */ + struct Statistics { + // Basic counters + size_t packets_processed = 0; + size_t frames_completed = 0; + size_t errors_recovered = 0; + size_t memory_copy_strategy_redetections = 0; + + // Enhanced frame tracking + size_t current_frame_number = 0; // Current frame being assembled + size_t frames_started = 0; // Total frames started (including dropped) + size_t frames_dropped = 0; // Frames dropped due to errors + size_t frames_completed_successfully = 0; // Successfully completed frames + + // Enhanced error tracking + size_t sequence_discontinuities = 0; // RTP sequence discontinuities + size_t buffer_overflow_errors = 0; // Buffer wraparound detections + size_t memory_corruption_errors = 0; // Memory bounds/corruption errors + size_t error_recovery_cycles = 0; // Number of error recovery cycles + + // Current frame metrics + size_t packets_in_current_frame = 0; // Packets accumulated in current frame + size_t bytes_in_current_frame = 0; // Bytes accumulated in current frame + uint32_t last_sequence_number = 0; // Last processed sequence number + uint32_t first_sequence_in_frame = 0; // First sequence number in current frame + + // State information + std::string current_strategy = "UNKNOWN"; + std::string current_frame_state = "IDLE"; + std::string last_error; + + // Timing information (for frame rates/debugging) +#if ENABLE_STATISTICS_LOGGING + std::chrono::steady_clock::time_point last_frame_completion_time; + std::chrono::steady_clock::time_point last_error_time; +#endif + }; + + Statistics get_statistics() const; + + /** + * @brief Get comprehensive statistics summary for debugging + * @return Formatted string with detailed statistics + */ + std::string get_statistics_summary() const; + + /** + * @brief Check if Media Frame Assembler has accumulated data waiting to be copied + * @return True if copy operations have accumulated data pending + */ + bool has_accumulated_data() const; + + /** + * @brief Get current frame for external operations (debugging) + * @return Current frame buffer or nullptr + */ + std::shared_ptr get_current_frame() const; + + /** + * @brief Get current frame position (debugging) + * @return Current byte position in frame + */ + size_t get_frame_position() const; + + private: + /** + * @brief Determine assembly controller event from packet parameters + * @param rtp_params RTP packet parameters + * @param payload Packet payload + * @return Appropriate state event + */ + StateEvent determine_event(const RtpParams& rtp_params, uint8_t* payload); + + /** + * @brief Execute actions based on assembly controller transition result + * @param result State transition result + * @param rtp_params RTP packet parameters + * @param payload Packet payload + */ + void execute_actions(const StateTransitionResult& result, const RtpParams& rtp_params, + uint8_t* payload); + + /** + * @brief Handle memory copy strategy detection and setup + * @param rtp_params RTP packet parameters + * @param payload Packet payload + * @return True if memory copy strategy is ready for processing + */ + bool handle_memory_copy_strategy_detection(const RtpParams& rtp_params, uint8_t* payload); + + /** + * @brief Set up memory copy strategy once detection is complete + * @param strategy Detected memory copy strategy + */ + void setup_memory_copy_strategy(std::unique_ptr strategy); + + /** + * @brief Validate packet integrity + * @param rtp_params RTP packet parameters + * @return True if packet is valid + */ + bool validate_packet_integrity(const RtpParams& rtp_params); + + /** + * @brief Handle frame completion processing + */ + void handle_frame_completion(); + + /** + * @brief Handle error recovery + * @param error_message Error description + */ + void handle_error_recovery(const std::string& error_message); + + /** + * @brief Update statistics + * @param event State event that occurred + */ + void update_statistics(StateEvent event); + + /** + * @brief Update packet-specific statistics + * @param rtp_params RTP parameters from the packet + */ + void update_packet_statistics(const RtpParams& rtp_params); + + private: + // Core components + std::unique_ptr assembly_controller_; + std::unique_ptr memory_copy_strategy_detector_; + std::unique_ptr current_copy_strategy_; + + // Configuration + AssemblerConfiguration config_; + + // Callback handlers + std::shared_ptr completion_handler_; + + // Statistics + mutable Statistics statistics_; + + // State tracking + bool memory_copy_strategy_detection_active_ = false; +}; + +/** + * @brief Default frame completion handler that can be used with the Media Frame Assembler + */ +class DefaultFrameCompletionHandler : public IFrameCompletionHandler { + public: + /** + * @brief Constructor + * @param frame_ready_callback Callback for completed frames + * @param error_callback Callback for errors + */ + DefaultFrameCompletionHandler( + std::function)> frame_ready_callback, + std::function error_callback = nullptr); + + // IFrameCompletionHandler interface + void on_frame_completed(std::shared_ptr frame) override; + void on_frame_error(const std::string& error_message) override; + + private: + std::function)> frame_ready_callback_; + std::function error_callback_; +}; + +/** + * @brief Utility functions for assembler configuration + */ +class AssemblerConfigurationHelper { + public: + /** + * @brief Create configuration with burst parameters + * @param header_stride Header stride size + * @param payload_stride Payload stride size + * @param hds_enabled HDS setting + * @param payload_on_cpu Whether payload is in CPU memory + * @param frames_on_host Whether frames should be in host memory + * @return Assembler configuration + */ + static AssemblerConfiguration create_with_burst_parameters(size_t header_stride, + size_t payload_stride, + bool hds_enabled, bool payload_on_cpu, + bool frames_on_host); + + /** + * @brief Create configuration for testing with forced memory copy strategy + * @param force_contiguous Whether to force contiguous memory copy strategy + * @return Test configuration + */ + static AssemblerConfiguration create_test_config(bool force_contiguous = true); + + /** + * @brief Validate configuration parameters + * @param config Configuration to validate + * @return True if configuration is valid + */ + static bool validate_configuration(const AssemblerConfiguration& config); +}; + +} // namespace holoscan::ops + +#endif // OPERATORS_ADVANCED_NETWORK_MEDIA_RX_MEDIA_FRAME_ASSEMBLER_H_ diff --git a/operators/advanced_network_media/advanced_network_media_rx/memory_copy_strategies.cpp b/operators/advanced_network_media/advanced_network_media_rx/memory_copy_strategies.cpp new file mode 100644 index 0000000000..b1223a4d0c --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_rx/memory_copy_strategies.cpp @@ -0,0 +1,685 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "memory_copy_strategies.h" +#include "../common/adv_network_media_common.h" +#include +#include + +namespace holoscan::ops { +namespace detail { + +// ======================================================================================== +// Memory Copy StrategyFactory Implementation +// ======================================================================================== + +std::unique_ptr StrategyFactory::create_detector() { + return std::make_unique(); +} + +std::unique_ptr StrategyFactory::create_contiguous_strategy( + nvidia::gxf::MemoryStorageType src_storage_type, + nvidia::gxf::MemoryStorageType dst_storage_type) { + return std::make_unique(src_storage_type, dst_storage_type); +} + +std::unique_ptr StrategyFactory::create_strided_strategy( + const StrideInfo& stride_info, nvidia::gxf::MemoryStorageType src_storage_type, + nvidia::gxf::MemoryStorageType dst_storage_type) { + return std::make_unique( + stride_info, src_storage_type, dst_storage_type); +} + +// ======================================================================================== +// Memory Copy StrategyDetector Implementation +// ======================================================================================== + +void MemoryCopyStrategyDetector::configure_burst_parameters(size_t header_stride_size, + size_t payload_stride_size, + bool hds_enabled) { + if (detection_complete_) { + ANM_STRATEGY_LOG("Strategy already detected, ignoring burst parameter update"); + return; + } + + // Check if configuration changed during detection + if (packets_analyzed_ > 0) { + bool config_changed = + (expected_header_stride_ != header_stride_size || + expected_payload_stride_ != payload_stride_size || hds_enabled_ != hds_enabled); + if (config_changed) { + ANM_STRATEGY_LOG("Burst configuration changed during detection, restarting analysis"); + reset(); + } + } + + expected_header_stride_ = header_stride_size; + expected_payload_stride_ = payload_stride_size; + hds_enabled_ = hds_enabled; + + ANM_STRATEGY_LOG( + "Strategy detector configured: header_stride={}, payload_stride={}, hds_enabled={}", + header_stride_size, + payload_stride_size, + hds_enabled); +} + +bool MemoryCopyStrategyDetector::collect_packet(const RtpParams& rtp_params, uint8_t* payload, + size_t payload_size) { + if (detection_complete_) { + return true; + } + + // Validate input + if (!payload || payload_size == 0) { + ANM_LOG_WARN("Invalid packet data for strategy detection, skipping"); + return false; + } + + // Store packet information + collected_payloads_.push_back(payload); + collected_payload_sizes_.push_back(payload_size); + collected_sequences_.push_back(rtp_params.sequence_number); + packets_analyzed_++; + + ANM_STRATEGY_LOG("Collected packet {} for detection: payload={}, size={}, seq={}", + packets_analyzed_, + static_cast(payload), + payload_size, + rtp_params.sequence_number); + + return packets_analyzed_ >= DETECTION_PACKET_COUNT; +} + +std::unique_ptr MemoryCopyStrategyDetector::detect_strategy( + nvidia::gxf::MemoryStorageType src_storage_type, + nvidia::gxf::MemoryStorageType dst_storage_type) { + if (collected_payloads_.size() < 2) { + ANM_STRATEGY_LOG("Insufficient packets for analysis ({} < 2), defaulting to CONTIGUOUS", + collected_payloads_.size()); + detection_complete_ = true; + return StrategyFactory::create_contiguous_strategy(src_storage_type, dst_storage_type); + } + + // Validate sequence continuity + if (!validate_sequence_continuity()) { + ANM_STRATEGY_LOG("RTP sequence drops detected, restarting detection"); + reset(); + return nullptr; + } + + // Check for buffer wraparound + if (detect_buffer_wraparound()) { + ANM_STRATEGY_LOG("Buffer wraparound detected, restarting detection"); + reset(); + return nullptr; + } + + // Analyze pattern + auto analysis_result = analyze_pattern(); + if (!analysis_result) { + ANM_STRATEGY_LOG("Pattern analysis failed, restarting detection"); + reset(); + return nullptr; + } + + auto [strategy_type, stride_info] = *analysis_result; + detection_complete_ = true; + + ANM_STRATEGY_LOG("Strategy detection completed: {} (stride: {}, payload: {})", + strategy_type == CopyStrategy::CONTIGUOUS ? "CONTIGUOUS" : "STRIDED", + stride_info.stride_size, + stride_info.payload_size); + + if (strategy_type == CopyStrategy::CONTIGUOUS) { + return StrategyFactory::create_contiguous_strategy(src_storage_type, dst_storage_type); + } else { + return StrategyFactory::create_strided_strategy( + stride_info, src_storage_type, dst_storage_type); + } +} + +void MemoryCopyStrategyDetector::reset() { + collected_payloads_.clear(); + collected_payload_sizes_.clear(); + collected_sequences_.clear(); + packets_analyzed_ = 0; + detection_complete_ = false; + + ANM_STRATEGY_LOG("Strategy detector reset"); +} + +std::optional> MemoryCopyStrategyDetector::analyze_pattern() { + if (collected_payloads_.size() < 2) { + return std::nullopt; + } + + // Verify payload size consistency + size_t payload_size = collected_payload_sizes_[0]; + for (size_t i = 1; i < collected_payload_sizes_.size(); ++i) { + if (collected_payload_sizes_[i] != payload_size) { + ANM_STRATEGY_LOG("Inconsistent payload sizes: first={}, packet_{}={}", + payload_size, + i, + collected_payload_sizes_[i]); + return std::nullopt; + } + } + + // Analyze memory layout + bool is_exactly_contiguous = true; + bool is_stride_consistent = true; + size_t actual_stride = 0; + + for (size_t i = 1; i < collected_payloads_.size(); ++i) { + uint8_t* prev_ptr = collected_payloads_[i - 1]; + uint8_t* curr_ptr = collected_payloads_[i]; + + size_t pointer_diff = curr_ptr - prev_ptr; + + if (i == 1) { + actual_stride = pointer_diff; + } + + // Check exact contiguity + uint8_t* expected_next_ptr = prev_ptr + payload_size; + if (curr_ptr != expected_next_ptr) { + is_exactly_contiguous = false; + ANM_STRATEGY_LOG("Non-contiguous detected: packet {}, expected={}, actual={}", + i, + static_cast(expected_next_ptr), + static_cast(curr_ptr)); + } + + // Check stride consistency + if (pointer_diff != actual_stride) { + is_stride_consistent = false; + ANM_STRATEGY_LOG( + "Inconsistent stride: packet {}, expected={}, actual={}", i, actual_stride, pointer_diff); + break; + } + } + + // Create stride info + StrideInfo stride_info; + stride_info.stride_size = actual_stride; + stride_info.payload_size = payload_size; + + // Determine memory copy strategy + CopyStrategy strategy; + if (is_exactly_contiguous) { + strategy = CopyStrategy::CONTIGUOUS; + ANM_STRATEGY_LOG("Packets are exactly contiguous, using CONTIGUOUS strategy"); + } else if (is_stride_consistent) { + strategy = CopyStrategy::STRIDED; + ANM_STRATEGY_LOG( + "Consistent stride pattern detected, using STRIDED strategy (stride={}, payload={})", + actual_stride, + payload_size); + } else { + strategy = CopyStrategy::CONTIGUOUS; + ANM_STRATEGY_LOG("Inconsistent patterns, falling back to CONTIGUOUS strategy"); + } + + return std::make_pair(strategy, stride_info); +} + +bool MemoryCopyStrategyDetector::validate_sequence_continuity() const { + if (collected_sequences_.size() < 2) { + return true; + } + + for (size_t i = 1; i < collected_sequences_.size(); ++i) { + uint64_t prev_seq = collected_sequences_[i - 1]; + uint64_t curr_seq = collected_sequences_[i]; + uint64_t expected_seq = prev_seq + 1; + + if (curr_seq != expected_seq) { + ANM_STRATEGY_LOG("RTP sequence discontinuity: expected {}, got {} (prev was {})", + expected_seq, + curr_seq, + prev_seq); + return false; + } + } + + return true; +} + +bool MemoryCopyStrategyDetector::detect_buffer_wraparound() const { + if (collected_payloads_.size() < 2) { + return false; + } + + for (size_t i = 1; i < collected_payloads_.size(); ++i) { + uint8_t* prev_ptr = collected_payloads_[i - 1]; + uint8_t* curr_ptr = collected_payloads_[i]; + + if (curr_ptr < prev_ptr) { + ptrdiff_t backward_diff = prev_ptr - curr_ptr; + if (backward_diff > 1024 * 1024) { // 1MB threshold + ANM_STRATEGY_LOG("Potential buffer wraparound: {} -> {}", + static_cast(prev_ptr), + static_cast(curr_ptr)); + return true; + } + } + } + + return false; +} + +// ======================================================================================== +// ContiguousMemoryCopyStrategy Implementation +// ======================================================================================== + +ContiguousMemoryCopyStrategy::ContiguousMemoryCopyStrategy( + nvidia::gxf::MemoryStorageType src_storage_type, + nvidia::gxf::MemoryStorageType dst_storage_type) + : src_storage_type_(src_storage_type), + dst_storage_type_(dst_storage_type), + copy_kind_(CopyOperationHelper::get_copy_kind(src_storage_type, dst_storage_type)) {} + +StateEvent ContiguousMemoryCopyStrategy::process_packet( + FrameAssemblyController& assembly_controller, uint8_t* payload, size_t payload_size) { + // Input validation for packet processing + if (!payload || payload_size == 0) { + ANM_LOG_ERROR("ContiguousStrategy: Invalid packet data"); + return StateEvent::CORRUPTION_DETECTED; + } + + // Initialize accumulation if needed + if (!accumulated_start_ptr_) { + accumulated_start_ptr_ = payload; + accumulated_size_ = 0; + ANM_MEMCOPY_TRACE("ContiguousStrategy: Starting new accumulation at {}", + static_cast(payload)); + } + + // Check memory contiguity + bool is_contiguous = (accumulated_start_ptr_ + accumulated_size_ == payload); + + if (!is_contiguous) { + ANM_MEMCOPY_TRACE("ContiguousStrategy: Contiguity break, executing copy for {} bytes", + accumulated_size_); + + // Execute accumulated copy before starting new accumulation + StateEvent copy_result = execute_copy(assembly_controller); + if (copy_result == StateEvent::CORRUPTION_DETECTED) { + return copy_result; + } + + // Start new accumulation + accumulated_start_ptr_ = payload; + accumulated_size_ = 0; + } + + // Add current packet to accumulation + accumulated_size_ += payload_size; + + // Safety check for frame bounds + auto frame = assembly_controller.get_current_frame(); + if (frame && accumulated_size_ > frame->get_size()) { + ANM_LOG_ERROR("ContiguousStrategy: Accumulated size ({}) exceeds frame size ({})", + accumulated_size_, + frame->get_size()); + reset(); + return StateEvent::CORRUPTION_DETECTED; + } + + return StateEvent::PACKET_ARRIVED; +} + +bool ContiguousMemoryCopyStrategy::has_accumulated_data() const { + return accumulated_size_ > 0 && accumulated_start_ptr_ != nullptr; +} + +void ContiguousMemoryCopyStrategy::reset() { + accumulated_start_ptr_ = nullptr; + accumulated_size_ = 0; + ANM_MEMCOPY_TRACE("ContiguousStrategy: Reset accumulation state"); +} + +StateEvent ContiguousMemoryCopyStrategy::execute_accumulated_copy( + FrameAssemblyController& assembly_controller) { + return execute_copy(assembly_controller); +} + +StateEvent ContiguousMemoryCopyStrategy::execute_copy( + FrameAssemblyController& assembly_controller) { + if (!has_accumulated_data()) { + return StateEvent::COPY_EXECUTED; + } + + // Validate copy bounds + if (!validate_copy_bounds(assembly_controller)) { + ANM_LOG_ERROR("ContiguousStrategy: Copy bounds validation failed"); + reset(); + return StateEvent::CORRUPTION_DETECTED; + } + + auto frame = assembly_controller.get_current_frame(); + uint8_t* dst_ptr = static_cast(frame->get()) + assembly_controller.get_frame_position(); + + ANM_MEMCOPY_TRACE("ContiguousStrategy: Executing copy - pos={}, size={}, frame_size={}", + assembly_controller.get_frame_position(), + accumulated_size_, + frame->get_size()); + + // Execute copy operation + if (!CopyOperationHelper::safe_copy( + dst_ptr, accumulated_start_ptr_, accumulated_size_, copy_kind_)) { + ANM_LOG_ERROR("ContiguousStrategy: Copy operation failed"); + reset(); + return StateEvent::CORRUPTION_DETECTED; + } + + // Update frame position + assembly_controller.advance_frame_position(accumulated_size_); + + ANM_MEMCOPY_TRACE("ContiguousStrategy: Copy completed - new_pos={}, copied={}", + assembly_controller.get_frame_position(), + accumulated_size_); + + // Reset accumulation state + reset(); + + return StateEvent::COPY_EXECUTED; +} + +bool ContiguousMemoryCopyStrategy::validate_copy_bounds( + FrameAssemblyController& assembly_controller) const { + auto frame = assembly_controller.get_current_frame(); + if (!frame) { + return false; + } + + size_t current_pos = assembly_controller.get_frame_position(); + size_t frame_size = frame->get_size(); + + return (current_pos + accumulated_size_ <= frame_size); +} + +// ======================================================================================== +// StridedMemoryCopyStrategy Implementation +// ======================================================================================== + +StridedMemoryCopyStrategy::StridedMemoryCopyStrategy( + const StrideInfo& stride_info, nvidia::gxf::MemoryStorageType src_storage_type, + nvidia::gxf::MemoryStorageType dst_storage_type) + : stride_info_(stride_info), + src_storage_type_(src_storage_type), + dst_storage_type_(dst_storage_type), + copy_kind_(CopyOperationHelper::get_copy_kind(src_storage_type, dst_storage_type)) {} + +StateEvent StridedMemoryCopyStrategy::process_packet(FrameAssemblyController& assembly_controller, + uint8_t* payload, size_t payload_size) { + // Input validation + if (!payload || payload_size == 0) { + ANM_LOG_ERROR("StridedStrategy: Invalid packet data"); + return StateEvent::CORRUPTION_DETECTED; + } + + // Initialize accumulation if needed + if (!first_packet_ptr_) { + reset_accumulation(payload, payload_size); + ANM_MEMCOPY_TRACE("StridedStrategy: Starting accumulation with first packet at {}", + static_cast(payload)); + return StateEvent::PACKET_ARRIVED; + } + + // Check if stride pattern is maintained + if (!is_stride_maintained(payload, payload_size)) { + ANM_MEMCOPY_TRACE("StridedStrategy: Stride pattern broken, executing copy and restarting"); + + // Execute accumulated copy if we have multiple packets + StateEvent copy_result; + if (accumulated_packet_count_ > 1) { + copy_result = execute_strided_copy(assembly_controller); + } else { + copy_result = execute_individual_copy( + assembly_controller, first_packet_ptr_, stride_info_.payload_size); + } + + if (copy_result == StateEvent::CORRUPTION_DETECTED) { + return copy_result; + } + + // Reset and start with current packet + reset_accumulation(payload, payload_size); + return StateEvent::PACKET_ARRIVED; + } + + // Stride is maintained, continue accumulation + last_packet_ptr_ = payload; + accumulated_packet_count_++; + accumulated_data_size_ += payload_size; + + ANM_MEMCOPY_TRACE("StridedStrategy: Accumulated packet {}, total_size={}", + accumulated_packet_count_, + accumulated_data_size_); + + return StateEvent::PACKET_ARRIVED; +} + +bool StridedMemoryCopyStrategy::has_accumulated_data() const { + return accumulated_packet_count_ > 0 && first_packet_ptr_ != nullptr; +} + +StateEvent StridedMemoryCopyStrategy::execute_accumulated_copy( + FrameAssemblyController& assembly_controller) { + if (!has_accumulated_data()) { + return StateEvent::COPY_EXECUTED; + } + + // Execute accumulated copy if we have multiple packets + StateEvent copy_result; + if (accumulated_packet_count_ > 1 && stride_validated_) { + copy_result = execute_strided_copy(assembly_controller); + } else { + copy_result = + execute_individual_copy(assembly_controller, first_packet_ptr_, stride_info_.payload_size); + } + + if (copy_result == StateEvent::COPY_EXECUTED) { + reset(); + } + + return copy_result; +} + +void StridedMemoryCopyStrategy::reset() { + first_packet_ptr_ = nullptr; + last_packet_ptr_ = nullptr; + accumulated_packet_count_ = 0; + accumulated_data_size_ = 0; + stride_validated_ = false; + actual_stride_ = 0; + ANM_MEMCOPY_TRACE("StridedStrategy: Reset accumulation state"); +} + +bool StridedMemoryCopyStrategy::is_stride_maintained(uint8_t* payload, size_t payload_size) { + if (!last_packet_ptr_) { + // First stride check + return true; + } + + size_t actual_diff = payload - last_packet_ptr_; + + if (!stride_validated_) { + // First stride validation + actual_stride_ = actual_diff; + stride_validated_ = true; + + if (actual_stride_ != stride_info_.stride_size) { + ANM_MEMCOPY_TRACE("StridedStrategy: Actual stride ({}) differs from expected ({})", + actual_stride_, + stride_info_.stride_size); + } + + return true; + } else { + // Subsequent stride validation + if (actual_diff != actual_stride_) { + ANM_MEMCOPY_TRACE("StridedStrategy: Stride inconsistent: expected={}, actual={}", + actual_stride_, + actual_diff); + return false; + } + return true; + } +} + +StateEvent StridedMemoryCopyStrategy::execute_strided_copy( + FrameAssemblyController& assembly_controller) { + if (accumulated_packet_count_ <= 1 || !first_packet_ptr_) { + return StateEvent::COPY_EXECUTED; + } + + // Validate copy bounds + if (!validate_strided_copy_bounds(assembly_controller)) { + ANM_LOG_ERROR("StridedStrategy: Strided copy bounds validation failed"); + reset(); + return StateEvent::CORRUPTION_DETECTED; + } + + auto frame = assembly_controller.get_current_frame(); + uint8_t* dst_ptr = static_cast(frame->get()) + assembly_controller.get_frame_position(); + + // Setup 2D copy parameters + size_t width = stride_info_.payload_size; + size_t height = accumulated_packet_count_; + size_t src_pitch = actual_stride_; + size_t dst_pitch = width; // Contiguous destination + + ANM_MEMCOPY_TRACE( + "StridedStrategy: Executing 2D copy - width={}, height={}, src_pitch={}, dst_pitch={}", + width, + height, + src_pitch, + dst_pitch); + + // Execute 2D copy + if (!CopyOperationHelper::safe_copy_2d( + dst_ptr, dst_pitch, first_packet_ptr_, src_pitch, width, height, copy_kind_)) { + ANM_LOG_ERROR("StridedStrategy: 2D copy operation failed"); + reset(); + return StateEvent::CORRUPTION_DETECTED; + } + + // Update frame position + assembly_controller.advance_frame_position(accumulated_data_size_); + + ANM_MEMCOPY_TRACE("StridedStrategy: Strided copy completed - new_pos={}, copied={}", + assembly_controller.get_frame_position(), + accumulated_data_size_); + + // Reset accumulation + reset(); + + return StateEvent::COPY_EXECUTED; +} + +StateEvent StridedMemoryCopyStrategy::execute_individual_copy( + FrameAssemblyController& assembly_controller, uint8_t* payload, size_t payload_size) { + auto frame = assembly_controller.get_current_frame(); + if (!frame) { + return StateEvent::CORRUPTION_DETECTED; + } + + uint8_t* dst_ptr = static_cast(frame->get()) + assembly_controller.get_frame_position(); + + // Bounds checking + if (assembly_controller.get_frame_position() + payload_size > frame->get_size()) { + ANM_LOG_ERROR("StridedStrategy: Individual copy would exceed frame bounds"); + return StateEvent::CORRUPTION_DETECTED; + } + + ANM_MEMCOPY_TRACE("StridedStrategy: Executing individual copy - size={}", payload_size); + + // Execute copy + if (!CopyOperationHelper::safe_copy(dst_ptr, payload, payload_size, copy_kind_)) { + ANM_LOG_ERROR("StridedStrategy: Individual copy operation failed"); + return StateEvent::CORRUPTION_DETECTED; + } + + // Update frame position + assembly_controller.advance_frame_position(payload_size); + + return StateEvent::COPY_EXECUTED; +} + +bool StridedMemoryCopyStrategy::validate_strided_copy_bounds( + FrameAssemblyController& assembly_controller) const { + auto frame = assembly_controller.get_current_frame(); + if (!frame) { + return false; + } + + size_t total_copy_size = stride_info_.payload_size * accumulated_packet_count_; + size_t current_pos = assembly_controller.get_frame_position(); + size_t frame_size = frame->get_size(); + + return (current_pos + total_copy_size <= frame_size); +} + +void StridedMemoryCopyStrategy::reset_accumulation(uint8_t* payload, size_t payload_size) { + first_packet_ptr_ = payload; + last_packet_ptr_ = payload; + accumulated_packet_count_ = 1; + accumulated_data_size_ = payload_size; + stride_validated_ = false; + actual_stride_ = 0; +} + +// ======================================================================================== +// CopyOperationHelper Implementation +// ======================================================================================== + +cudaMemcpyKind CopyOperationHelper::get_copy_kind(nvidia::gxf::MemoryStorageType src_storage_type, + nvidia::gxf::MemoryStorageType dst_storage_type) { + if (src_storage_type == nvidia::gxf::MemoryStorageType::kHost) { + return (dst_storage_type == nvidia::gxf::MemoryStorageType::kHost) ? cudaMemcpyHostToHost + : cudaMemcpyHostToDevice; + } else { + return (dst_storage_type == nvidia::gxf::MemoryStorageType::kHost) ? cudaMemcpyDeviceToHost + : cudaMemcpyDeviceToDevice; + } +} + +bool CopyOperationHelper::safe_copy(void* dst, const void* src, size_t size, cudaMemcpyKind kind) { + if (!dst || !src || size == 0) { + ANM_LOG_ERROR("CopyOperationHelper: Invalid copy parameters"); + return false; + } + + try { + CUDA_TRY(cudaMemcpy(dst, src, size, kind)); + return true; + } catch (const std::exception& e) { + ANM_LOG_ERROR("CopyOperationHelper: Copy failed - {}", e.what()); + return false; + } +} + +bool CopyOperationHelper::safe_copy_2d(void* dst, size_t dst_pitch, const void* src, + size_t src_pitch, size_t width, size_t height, + cudaMemcpyKind kind) { + if (!dst || !src || width == 0 || height == 0) { + ANM_LOG_ERROR("CopyOperationHelper: Invalid 2D copy parameters"); + return false; + } + + try { + CUDA_TRY(cudaMemcpy2D(dst, dst_pitch, src, src_pitch, width, height, kind)); + return true; + } catch (const std::exception& e) { + ANM_LOG_ERROR("CopyOperationHelper: 2D copy failed - {}", e.what()); + return false; + } +} + +} // namespace detail +} // namespace holoscan::ops diff --git a/operators/advanced_network_media/advanced_network_media_rx/memory_copy_strategies.h b/operators/advanced_network_media/advanced_network_media_rx/memory_copy_strategies.h new file mode 100644 index 0000000000..59774124ed --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_rx/memory_copy_strategies.h @@ -0,0 +1,369 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef OPERATORS_ADVANCED_NETWORK_MEDIA_RX_MEMORY_COPY_STRATEGIES_H_ +#define OPERATORS_ADVANCED_NETWORK_MEDIA_RX_MEMORY_COPY_STRATEGIES_H_ + +#include +#include +#include +#include +#include +#include "frame_assembly_controller.h" +#include "../common/adv_network_media_common.h" + +namespace holoscan::ops { + +namespace detail { + +/** + * @brief Strategy types for copy operations + */ +enum class CopyStrategy { + UNKNOWN, // Memory copy strategy not yet determined + CONTIGUOUS, // Sequential memory copy strategy + STRIDED // Strided memory copy for HDS scenarios +}; + +/** + * @brief Strategy interface for memory copy operations + * + * This interface defines how different memory copy strategies handle packet data. + * While it coordinates with FrameAssemblyController, it belongs in the memory copy + * domain because its primary responsibility is memory transfer optimization. + */ +class IMemoryCopyStrategy { + public: + virtual ~IMemoryCopyStrategy() = default; + + /** + * @brief Process packet with assembly controller coordination + * @param assembly_controller Reference to frame assembly controller + * @param payload Packet payload data + * @param payload_size Size of payload + * @return Event generated by processing + */ + virtual StateEvent process_packet(FrameAssemblyController& assembly_controller, uint8_t* payload, + size_t payload_size) = 0; + + /** + * @brief Execute accumulated copy operations + * @param assembly_controller Reference to frame assembly controller for context updates + * @return Event indicating copy result + */ + virtual StateEvent execute_accumulated_copy(FrameAssemblyController& assembly_controller) = 0; + + /** + * @brief Check if strategy has accumulated data waiting to be copied + * @return True if accumulated data needs to be copied + */ + virtual bool has_accumulated_data() const = 0; + + /** + * @brief Get strategy type for debugging/statistics + * @return Strategy type identifier + */ + virtual CopyStrategy get_type() const = 0; + + /** + * @brief Reset strategy state + */ + virtual void reset() = 0; +}; + +/** + * @brief Strategy detection and creation factory + */ +class StrategyFactory { + public: + /** + * @brief Create strategy detector for pattern analysis + * @return Strategy detector instance + */ + static std::unique_ptr create_detector(); + + /** + * @brief Create contiguous strategy + * @param src_storage_type Source memory type + * @param dst_storage_type Destination memory type + * @return Contiguous strategy instance + */ + static std::unique_ptr create_contiguous_strategy( + nvidia::gxf::MemoryStorageType src_storage_type, + nvidia::gxf::MemoryStorageType dst_storage_type); + + /** + * @brief Create strided strategy + * @param stride_info Detected stride information + * @param src_storage_type Source memory type + * @param dst_storage_type Destination memory type + * @return Strided strategy instance + */ + static std::unique_ptr create_strided_strategy( + const StrideInfo& stride_info, nvidia::gxf::MemoryStorageType src_storage_type, + nvidia::gxf::MemoryStorageType dst_storage_type); +}; + +/** + * @brief Strategy detector for analyzing packet patterns + */ +class MemoryCopyStrategyDetector { + public: + static constexpr size_t DETECTION_PACKET_COUNT = 4; + + /** + * @brief Configure detector with burst parameters + * @param header_stride_size Header stride from burst info + * @param payload_stride_size Payload stride from burst info + * @param hds_enabled Whether header data split is enabled + */ + void configure_burst_parameters(size_t header_stride_size, size_t payload_stride_size, + bool hds_enabled); + + /** + * @brief Collect packet for analysis + * @param rtp_params RTP packet parameters + * @param payload Payload pointer + * @param payload_size Payload size + * @return True if enough packets collected for detection + */ + bool collect_packet(const RtpParams& rtp_params, uint8_t* payload, size_t payload_size); + + /** + * @brief Analyze collected packets and determine strategy + * @param src_storage_type Source memory storage type + * @param dst_storage_type Destination memory storage type + * @return Detected strategy or nullptr if detection failed + */ + std::unique_ptr detect_strategy( + nvidia::gxf::MemoryStorageType src_storage_type, + nvidia::gxf::MemoryStorageType dst_storage_type); + + /** + * @brief Check if detection is complete + * @return True if strategy has been determined + */ + bool is_detection_complete() const { return detection_complete_; } + + /** + * @brief Reset detector for new detection cycle + */ + void reset(); + + /** + * @brief Get number of packets analyzed + * @return Packet count + */ + size_t get_packets_analyzed() const { return packets_analyzed_; } + + private: + /** + * @brief Analyze collected packet pattern + * @return Analysis result with strategy type and stride info + */ + std::optional> analyze_pattern(); + + /** + * @brief Validate RTP sequence continuity + * @return True if no sequence drops detected + */ + bool validate_sequence_continuity() const; + + /** + * @brief Check for cyclic buffer wraparound + * @return True if wraparound detected + */ + bool detect_buffer_wraparound() const; + + private: + // Detection data + std::vector collected_payloads_; + std::vector collected_payload_sizes_; + std::vector collected_sequences_; + size_t packets_analyzed_ = 0; + bool detection_complete_ = false; + + // Burst configuration + size_t expected_header_stride_ = 0; + size_t expected_payload_stride_ = 0; + bool hds_enabled_ = false; +}; + +/** + * @brief Assembly controller aware contiguous strategy + */ +class ContiguousMemoryCopyStrategy : public IMemoryCopyStrategy { + public: + /** + * @brief Constructor + * @param src_storage_type Source memory storage type + * @param dst_storage_type Destination memory storage type + */ + ContiguousMemoryCopyStrategy(nvidia::gxf::MemoryStorageType src_storage_type, + nvidia::gxf::MemoryStorageType dst_storage_type); + + // IMemoryCopyStrategy interface + StateEvent process_packet(FrameAssemblyController& assembly_controller, uint8_t* payload, + size_t payload_size) override; + + StateEvent execute_accumulated_copy(FrameAssemblyController& assembly_controller) override; + + bool has_accumulated_data() const override; + void reset() override; + CopyStrategy get_type() const override { return CopyStrategy::CONTIGUOUS; } + + private: + /** + * @brief Execute accumulated copy operation + * @param assembly_controller Frame assembly controller reference + * @return State event result + */ + StateEvent execute_copy(FrameAssemblyController& assembly_controller); + + /** + * @brief Validate copy operation bounds + * @param assembly_controller Frame assembly controller reference + * @return True if copy is safe to execute + */ + bool validate_copy_bounds(FrameAssemblyController& assembly_controller) const; + + private: + uint8_t* accumulated_start_ptr_ = nullptr; + size_t accumulated_size_ = 0; + cudaMemcpyKind copy_kind_; + nvidia::gxf::MemoryStorageType src_storage_type_; + nvidia::gxf::MemoryStorageType dst_storage_type_; +}; + +/** + * @brief Assembly controller aware strided strategy + */ +class StridedMemoryCopyStrategy : public IMemoryCopyStrategy { + public: + /** + * @brief Constructor + * @param stride_info Stride pattern information + * @param src_storage_type Source memory storage type + * @param dst_storage_type Destination memory storage type + */ + StridedMemoryCopyStrategy(const StrideInfo& stride_info, + nvidia::gxf::MemoryStorageType src_storage_type, + nvidia::gxf::MemoryStorageType dst_storage_type); + + // IMemoryCopyStrategy interface + StateEvent process_packet(FrameAssemblyController& assembly_controller, uint8_t* payload, + size_t payload_size) override; + + StateEvent execute_accumulated_copy(FrameAssemblyController& assembly_controller) override; + + bool has_accumulated_data() const override; + void reset() override; + CopyStrategy get_type() const override { return CopyStrategy::STRIDED; } + + private: + /** + * @brief Check if stride pattern is maintained + * @param payload Current packet payload pointer + * @param payload_size Current packet payload size + * @return True if stride is consistent + */ + bool is_stride_maintained(uint8_t* payload, size_t payload_size); + + /** + * @brief Execute strided copy operation + * @param assembly_controller Frame assembly controller reference + * @return State event result + */ + StateEvent execute_strided_copy(FrameAssemblyController& assembly_controller); + + /** + * @brief Execute individual packet copy (fallback) + * @param assembly_controller Frame assembly controller reference + * @param payload Packet payload + * @param payload_size Payload size + * @return State event result + */ + StateEvent execute_individual_copy(FrameAssemblyController& assembly_controller, uint8_t* payload, + size_t payload_size); + + /** + * @brief Validate strided copy bounds + * @param assembly_controller Frame assembly controller reference + * @return True if copy is safe to execute + */ + bool validate_strided_copy_bounds(FrameAssemblyController& assembly_controller) const; + + /** + * @brief Reset accumulation state for new pattern + * @param payload New packet payload + * @param payload_size New packet size + */ + void reset_accumulation(uint8_t* payload, size_t payload_size); + + private: + StrideInfo stride_info_; + + // Accumulation state + uint8_t* first_packet_ptr_ = nullptr; + uint8_t* last_packet_ptr_ = nullptr; + size_t accumulated_packet_count_ = 0; + size_t accumulated_data_size_ = 0; + + // Stride validation + bool stride_validated_ = false; + size_t actual_stride_ = 0; + + // Memory configuration + cudaMemcpyKind copy_kind_; + nvidia::gxf::MemoryStorageType src_storage_type_; + nvidia::gxf::MemoryStorageType dst_storage_type_; + + // Wraparound detection threshold + static constexpr size_t WRAPAROUND_THRESHOLD = 1024 * 1024; +}; + +/** + * @brief Helper functions for memory copy operations + */ +class CopyOperationHelper { + public: + /** + * @brief Determine appropriate copy kind + * @param src_storage_type Source memory type + * @param dst_storage_type Destination memory type + * @return CUDA copy kind + */ + static cudaMemcpyKind get_copy_kind(nvidia::gxf::MemoryStorageType src_storage_type, + nvidia::gxf::MemoryStorageType dst_storage_type); + + /** + * @brief Execute safe memory copy with error handling + * @param dst Destination pointer + * @param src Source pointer + * @param size Copy size + * @param kind Copy kind + * @return True if copy succeeded + */ + static bool safe_copy(void* dst, const void* src, size_t size, cudaMemcpyKind kind); + + /** + * @brief Execute safe 2D memory copy with error handling + * @param dst Destination pointer + * @param dst_pitch Destination pitch + * @param src Source pointer + * @param src_pitch Source pitch + * @param width Copy width + * @param height Copy height + * @param kind Copy kind + * @return True if copy succeeded + */ + static bool safe_copy_2d(void* dst, size_t dst_pitch, const void* src, size_t src_pitch, + size_t width, size_t height, cudaMemcpyKind kind); +}; + +} // namespace detail +} // namespace holoscan::ops + +#endif // OPERATORS_ADVANCED_NETWORK_MEDIA_RX_MEMORY_COPY_STRATEGIES_H_ diff --git a/operators/advanced_network_media/advanced_network_media_rx/metadata.json b/operators/advanced_network_media/advanced_network_media_rx/metadata.json new file mode 100644 index 0000000000..d253bc82da --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_rx/metadata.json @@ -0,0 +1,32 @@ +{ + "operator": { + "name": "advanced_network_media_rx", + "authors": [ + { + "name": "Rony Rado", + "affiliation": "NVIDIA" + } + ], + "version": "1.0", + "changelog": { + "1.0": "Initial Release" + }, + "language": ["C++", "Python"], + "platforms": ["x86_64", "aarch64"], + "tags": ["Networking and Distributed Computing", "DPDK", "UDP", "Ethernet", "IP", "GPUDirect", "Rivermax"], + "holoscan_sdk": { + "minimum_required_version": "2.6.0", + "tested_versions": [ + "2.6.0" + ] + }, + "ranking": 3, + "requirements": { + "operators": [{ + "name": "advanced_network", + "version": "1.4" + } + ] + } + } +} \ No newline at end of file diff --git a/operators/advanced_network_media/advanced_network_media_rx/network_burst_processor.cpp b/operators/advanced_network_media/advanced_network_media_rx/network_burst_processor.cpp new file mode 100644 index 0000000000..40cb696b77 --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_rx/network_burst_processor.cpp @@ -0,0 +1,131 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "network_burst_processor.h" +#include "../common/adv_network_media_common.h" +#include "advanced_network/common.h" + +namespace holoscan::ops { + +namespace ano = holoscan::advanced_network; +using holoscan::advanced_network::AnoBurstExtendedInfo; +using holoscan::advanced_network::BurstParams; + +NetworkBurstProcessor::NetworkBurstProcessor( + std::shared_ptr assembler) + : assembler_(assembler) { + if (!assembler_) { + throw std::invalid_argument("MediaFrameAssembler cannot be null"); + } +} + +void NetworkBurstProcessor::process_burst(BurstParams* burst) { + if (!burst || burst->hdr.hdr.num_pkts == 0) { + return; + } + + // Process each packet through the frame assembler + for (size_t i = 0; i < burst->hdr.hdr.num_pkts; ++i) { + auto extraction_result = extract_packet_data(burst, i); + + // Skip packet if extraction failed + if (!extraction_result) { + ANM_LOG_WARN("Failed to extract payload from packet {}", i); + continue; + } + + ANM_PACKET_TRACE("About to process packet {}/{}: seq={}, m_bit={}, size={}, payload_ptr={}", + i + 1, + burst->hdr.hdr.num_pkts, + extraction_result.rtp_params.sequence_number, + extraction_result.rtp_params.m_bit, + extraction_result.rtp_params.payload_size, + static_cast(extraction_result.payload)); + + assembler_->process_incoming_packet(extraction_result.rtp_params, extraction_result.payload); + + ANM_PACKET_TRACE("Processed packet {}/{}: seq={}, m_bit={}, size={}", + i + 1, + burst->hdr.hdr.num_pkts, + extraction_result.rtp_params.sequence_number, + extraction_result.rtp_params.m_bit, + extraction_result.rtp_params.payload_size); + } +} + +PacketExtractionResult NetworkBurstProcessor::extract_packet_data(BurstParams* burst, + size_t packet_index) { + PacketExtractionResult result; + + if (packet_index >= burst->hdr.hdr.num_pkts) { + ANM_LOG_ERROR( + "Packet index {} out of range (max: {})", packet_index, burst->hdr.hdr.num_pkts); + return result; // success = false, payload = nullptr + } + + // Get HDS configuration from burst data + const auto* burst_info = + reinterpret_cast(&(burst->hdr.custom_burst_data)); + + if (burst_info->hds_on) { + // Header-Data Split mode: headers on CPU, payloads on GPU + uint8_t* header_ptr = reinterpret_cast(burst->pkts[CPU_PKTS][packet_index]); + uint8_t* payload_ptr = reinterpret_cast(burst->pkts[GPU_PKTS][packet_index]); + + if (!header_ptr || !payload_ptr) { + ANM_LOG_ERROR("Null pointer in HDS packet {}: header={}, payload={}", + packet_index, + static_cast(header_ptr), + static_cast(payload_ptr)); + return result; // success = false, payload = nullptr + } + + // Parse RTP header from CPU memory + if (!parse_rtp_header(header_ptr, result.rtp_params)) { + ANM_LOG_ERROR("Failed to parse RTP header for packet {}", packet_index); + return result; // success = false, payload = nullptr + } + + ANM_PACKET_TRACE("HDS packet {}: header_ptr={}, payload_ptr={}, seq={}", + packet_index, + static_cast(header_ptr), + static_cast(payload_ptr), + result.rtp_params.sequence_number); + + result.payload = payload_ptr; + result.success = true; + return result; + + } else { + // Standard mode: complete packet on CPU + uint8_t* packet_ptr = reinterpret_cast(burst->pkts[CPU_PKTS][packet_index]); + + if (!packet_ptr) { + ANM_LOG_ERROR("Null packet pointer for packet {}", packet_index); + return result; // success = false, payload = nullptr + } + + // Parse RTP header from beginning of packet + if (!parse_rtp_header(packet_ptr, result.rtp_params)) { + ANM_LOG_ERROR("Failed to parse RTP header for packet {}", packet_index); + return result; // success = false, payload = nullptr + } + + // Payload starts after RTP header + uint8_t* payload_ptr = packet_ptr + RTP_SINGLE_SRD_HEADER_SIZE; + + ANM_PACKET_TRACE("Standard packet {}: packet_ptr={}, payload_ptr={}, seq={}", + packet_index, + static_cast(packet_ptr), + static_cast(payload_ptr), + result.rtp_params.sequence_number); + + result.payload = payload_ptr; + result.success = true; + return result; + } +} + +} // namespace holoscan::ops diff --git a/operators/advanced_network_media/advanced_network_media_rx/network_burst_processor.h b/operators/advanced_network_media/advanced_network_media_rx/network_burst_processor.h new file mode 100644 index 0000000000..31cf977d84 --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_rx/network_burst_processor.h @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef OPERATORS_ADVANCED_NETWORK_MEDIA_RX_NETWORK_BURST_PROCESSOR_H_ +#define OPERATORS_ADVANCED_NETWORK_MEDIA_RX_NETWORK_BURST_PROCESSOR_H_ + +#include +#include "holoscan/logger/logger.hpp" +#include "advanced_network/common.h" +#include "advanced_network/managers/rivermax/rivermax_ano_data_types.h" +#include "media_frame_assembler.h" +#include "../common/rtp_params.h" + +namespace holoscan::ops { + +// Targeted using declarations for specific types from advanced_network namespace +using holoscan::advanced_network::BurstParams; + +/** + * @brief Result structure for packet data extraction + */ +struct PacketExtractionResult { + uint8_t* payload = nullptr; ///< Pointer to packet payload data + RtpParams rtp_params; ///< Extracted RTP parameters + bool success = false; ///< Whether extraction was successful + + /// Implicit conversion to bool for easy error checking + explicit operator bool() const { return success && payload != nullptr; } +}; + +/** + * @brief Network burst processor that integrates with the frame assembler + * + * This class handles burst-level operations and forwards individual packets + * to the MediaFrameAssembler for frame assembly processing. + */ +class NetworkBurstProcessor { + public: + /** + * @brief Constructor + * @param assembler The frame assembler with assembly controller + */ + explicit NetworkBurstProcessor(std::shared_ptr assembler); + + /** + * @brief Process a burst of packets + * @param burst The burst containing packets to process + * @note Assembler must be configured before calling this method + */ + void process_burst(BurstParams* burst); + + private: + /** + * @brief Extract RTP header and payload from packet + * @param burst The burst containing packets + * @param packet_index Index of packet in burst + * @return PacketExtractionResult containing payload pointer, RTP parameters, and success status + */ + PacketExtractionResult extract_packet_data(BurstParams* burst, size_t packet_index); + + private: + // Constants for packet array indexing + static constexpr int CPU_PKTS = 0; + static constexpr int GPU_PKTS = 1; + + std::shared_ptr assembler_; +}; + +} // namespace holoscan::ops + +#endif // OPERATORS_ADVANCED_NETWORK_MEDIA_RX_NETWORK_BURST_PROCESSOR_H_ diff --git a/operators/advanced_network_media/advanced_network_media_rx/python/CMakeLists.txt b/operators/advanced_network_media/advanced_network_media_rx/python/CMakeLists.txt new file mode 100644 index 0000000000..96f4be6fa5 --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_rx/python/CMakeLists.txt @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +include(pybind11_add_holohub_module) +find_package(holoscan 2.6 REQUIRED CONFIG + PATHS "/opt/nvidia/holoscan" "/workspace/holoscan-sdk/install") + +set(MODULE_NAME advanced_network_media_rx) +set(MODULE_CLASS_NAME "*") + +pybind11_add_holohub_module( + CPP_CMAKE_TARGET advanced_network_media_rx + CLASS_NAME "AdvNetworkMediaRxOp" + SOURCES adv_network_media_rx_pybind.cpp +) + +target_link_libraries(${MODULE_NAME}_python +PRIVATE + holoscan::core + ${MODULE_NAME} +) + +set(CMAKE_PYBIND11_HOLOHUB_MODULE_OUT_DIR ${CMAKE_BINARY_DIR}/python/${CMAKE_INSTALL_LIBDIR}/holohub) +set(CMAKE_SUBMODULE_OUT_DIR ${CMAKE_PYBIND11_HOLOHUB_MODULE_OUT_DIR}/${MODULE_NAME}) + +set_target_properties(${MODULE_NAME}_python PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SUBMODULE_OUT_DIR} + OUTPUT_NAME _${MODULE_NAME} +) + +configure_file( + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/pybind11/__init__.py + ${CMAKE_PYBIND11_HOLOHUB_MODULE_OUT_DIR}/advanced_network_media_rx/__init__.py +) diff --git a/operators/advanced_network_media/advanced_network_media_rx/python/adv_network_media_rx_pybind.cpp b/operators/advanced_network_media/advanced_network_media_rx/python/adv_network_media_rx_pybind.cpp new file mode 100644 index 0000000000..202eac1da7 --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_rx/python/adv_network_media_rx_pybind.cpp @@ -0,0 +1,139 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "../adv_network_media_rx.h" +#include "./adv_network_media_rx_pydoc.hpp" + +#include +#include // for unordered_map -> dict, etc. + +#include +#include +#include + +#include "../../../operator_util.hpp" +#include +#include +#include +#include + +// Add advanced network headers +#include "advanced_network/common.h" +#include "advanced_network/types.h" + +using std::string_literals::operator""s; +using pybind11::literals::operator""_a; + +#define STRINGIFY(x) #x +#define MACRO_STRINGIFY(x) STRINGIFY(x) + +namespace py = pybind11; + +namespace holoscan::ops { + +/* Trampoline classes for handling Python kwargs + * + * These add a constructor that takes a Fragment for which to initialize the operator. + * The explicit parameter list and default arguments take care of providing a Pythonic + * kwarg-based interface with appropriate default values matching the operator's + * default parameters in the C++ API `setup` method. + * + * The sequence of events in this constructor is based on Fragment::make_operator + */ + +class PyAdvNetworkMediaRxOp : public AdvNetworkMediaRxOp { + public: + /* Inherit the constructors */ + using AdvNetworkMediaRxOp::AdvNetworkMediaRxOp; + + // Define a constructor that fully initializes the object. + PyAdvNetworkMediaRxOp(Fragment* fragment, const py::args& args, + const std::string& interface_name = "", + uint16_t queue_id = default_queue_id, uint32_t frame_width = 1920, + uint32_t frame_height = 1080, uint32_t bit_depth = 8, + const std::string& video_format = "RGB888", bool hds = true, + const std::string& output_format = "video_buffer", + const std::string& memory_location = "device", + const std::string& name = "advanced_network_media_rx") { + add_positional_condition_and_resource_args(this, args); + name_ = name; + fragment_ = fragment; + spec_ = std::make_shared(fragment); + setup(*spec_.get()); + + // Set parameters if provided + if (!interface_name.empty()) { this->add_arg(Arg("interface_name", interface_name)); } + this->add_arg(Arg("queue_id", queue_id)); + this->add_arg(Arg("frame_width", frame_width)); + this->add_arg(Arg("frame_height", frame_height)); + this->add_arg(Arg("bit_depth", bit_depth)); + this->add_arg(Arg("video_format", video_format)); + this->add_arg(Arg("hds", hds)); + this->add_arg(Arg("output_format", output_format)); + this->add_arg(Arg("memory_location", memory_location)); + } +}; + +PYBIND11_MODULE(_advanced_network_media_rx, m) { + m.doc() = R"pbdoc( + Holoscan SDK Advanced Networking Media RX Operator Python Bindings + ------------------------------------------------------------------ + .. currentmodule:: _advanced_network_media_rx + + This module provides Python bindings for the Advanced Networking Media RX operator, + which receives video frames over Rivermax-enabled network infrastructure. + )pbdoc"; + +#ifdef VERSION_INFO + m.attr("__version__") = MACRO_STRINGIFY(VERSION_INFO); +#else + m.attr("__version__") = "dev"; +#endif + + py::class_>( + m, "AdvNetworkMediaRxOp", doc::AdvNetworkMediaRxOp::doc_AdvNetworkMediaRxOp) + .def(py::init(), + "fragment"_a, + "interface_name"_a = ""s, + "queue_id"_a = AdvNetworkMediaRxOp::default_queue_id, + "frame_width"_a = 1920, + "frame_height"_a = 1080, + "bit_depth"_a = 8, + "video_format"_a = "RGB888"s, + "hds"_a = true, + "output_format"_a = "video_buffer"s, + "memory_location"_a = "device"s, + "name"_a = "advanced_network_media_rx"s, + doc::AdvNetworkMediaRxOp::doc_AdvNetworkMediaRxOp_python) + .def("initialize", &AdvNetworkMediaRxOp::initialize, doc::AdvNetworkMediaRxOp::doc_initialize) + .def("setup", &AdvNetworkMediaRxOp::setup, "spec"_a, doc::AdvNetworkMediaRxOp::doc_setup); +} // PYBIND11_MODULE NOLINT +} // namespace holoscan::ops diff --git a/operators/advanced_network_media/advanced_network_media_rx/python/adv_network_media_rx_pydoc.hpp b/operators/advanced_network_media/advanced_network_media_rx/python/adv_network_media_rx_pydoc.hpp new file mode 100644 index 0000000000..1897faa735 --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_rx/python/adv_network_media_rx_pydoc.hpp @@ -0,0 +1,101 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef PYHOLOHUB_OPERATORS_ADV_NET_MEDIA_RX_PYDOC_HPP +#define PYHOLOHUB_OPERATORS_ADV_NET_MEDIA_RX_PYDOC_HPP + +#include + +#include "macros.hpp" + +namespace holoscan::doc { + +namespace AdvNetworkMediaRxOp { + +PYDOC(AdvNetworkMediaRxOp, R"doc( +Advanced Networking Media Receiver operator. + +This operator receives video frames over Rivermax-enabled network infrastructure +and outputs them as GXF VideoBuffer entities. +)doc") + +// PyAdvNetworkMediaRxOp Constructor +PYDOC(AdvNetworkMediaRxOp_python, R"doc( +Advanced Networking Media Receiver operator. + +This operator receives video frames over Rivermax-enabled network infrastructure +and outputs them as GXF VideoBuffer entities. + +Note: Advanced network initialization must be done in the application before creating +this operator using: adv_network_common.adv_net_init(config) + +Parameters +---------- +fragment : Fragment + The fragment that the operator belongs to. +interface_name : str, optional + Name of the network interface to use for reception. +queue_id : int, optional + Queue ID for the network interface (default: 0). +frame_width : int, optional + Width of the video frame in pixels (default: 1920). +frame_height : int, optional + Height of the video frame in pixels (default: 1080). +bit_depth : int, optional + Bit depth of the video data (default: 8). +video_format : str, optional + Video format for reception (default: "RGB888"). +hds : bool, optional + Header Data Split setting (default: True). +output_format : str, optional + Output format for the frames (default: "video_buffer"). +memory_location : str, optional + Memory location for frame storage (default: "device"). +name : str, optional + The name of the operator (default: "advanced_network_media_rx"). +)doc") + +PYDOC(gxf_typename, R"doc( +The GXF type name of the resource. + +Returns +------- +str + The GXF type name of the resource +)doc") + +PYDOC(initialize, R"doc( +Initialize the operator. + +This method is called only once when the operator is created for the first time, +and uses a light-weight initialization. +)doc") + +PYDOC(setup, R"doc( +Define the operator specification. + +Parameters +---------- +spec : ``holoscan.core.OperatorSpec`` + The operator specification. +)doc") + +} // namespace AdvNetworkMediaRxOp + +} // namespace holoscan::doc + +#endif // PYHOLOHUB_OPERATORS_ADV_NET_MEDIA_RX_PYDOC_HPP diff --git a/operators/advanced_network_media/advanced_network_media_tx/CMakeLists.txt b/operators/advanced_network_media/advanced_network_media_tx/CMakeLists.txt new file mode 100644 index 0000000000..6a7a27417f --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_tx/CMakeLists.txt @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +cmake_minimum_required(VERSION 3.20) +project(advanced_network_media_tx) + +find_package(holoscan 2.6 REQUIRED CONFIG + PATHS "/opt/nvidia/holoscan" "/workspace/holoscan-sdk/install") + +add_library(${PROJECT_NAME} SHARED + adv_network_media_tx.cpp +) + +add_library(holoscan::ops::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) + +target_include_directories(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../../advanced_network + ${CMAKE_CURRENT_SOURCE_DIR}/../../advanced_network/advanced_network +) + +target_link_libraries(${PROJECT_NAME} + PRIVATE + holoscan::core + GXF::multimedia + rivermax-dev-kit + advanced_network_common + advanced_network_media_common +) + +set_target_properties(${PROJECT_NAME} PROPERTIES + OUTPUT_NAME "holoscan_op_advanced_network_media_tx" + EXPORT_NAME ops::advanced_network_media_tx +) + +# Installation +install( + TARGETS + ${PROJECT_NAME} + EXPORT holoscan-networking-targets + COMPONENT advanced_network-cpp +) + +if(HOLOHUB_BUILD_PYTHON) + add_subdirectory(python) +endif() diff --git a/operators/advanced_network_media/advanced_network_media_tx/adv_network_media_tx.cpp b/operators/advanced_network_media/advanced_network_media_tx/adv_network_media_tx.cpp new file mode 100644 index 0000000000..192622f1f3 --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_tx/adv_network_media_tx.cpp @@ -0,0 +1,318 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "advanced_network/common.h" + +#include +#include "../common/frame_buffer.h" +#include "../common/video_parameters.h" +#include "../common/adv_network_media_logging.h" +#include "adv_network_media_tx.h" + +namespace holoscan::ops { + +namespace ano = holoscan::advanced_network; + +using holoscan::advanced_network::BurstParams; +using holoscan::advanced_network::Status; + +/** + * @class AdvNetworkMediaTxOpImpl + * @brief Implementation class for the AdvNetworkMediaTxOp operator. + * + * Handles the actual processing of media frames and communication with + * the network infrastructure. + */ +class AdvNetworkMediaTxOpImpl { + public: + static constexpr int DISPLAY_WARNING_AFTER_BURST_NOT_AVAILABLE = 1000; + static constexpr int MAX_RETRY_ATTEMPTS_BEFORE_DROP = 10; + static constexpr int SLEEP_WHEN_BURST_NOT_AVAILABLE_US = 100; + + /** + * @brief Constructs an implementation for the given operator. + * + * @param parent Reference to the parent operator. + */ + explicit AdvNetworkMediaTxOpImpl(AdvNetworkMediaTxOp& parent) : parent_(parent) {} + + /** + * @brief Initializes the implementation. + * + * Sets up the network port, calculates frame size, and prepares + * for media transmission. + */ + void initialize() { + ANM_LOG_INFO("AdvNetworkMediaTxOp::initialize()"); + try { + port_id_ = ano::get_port_id(parent_.interface_name_.get()); + if (port_id_ == -1) { + ANM_LOG_ERROR("Invalid TX port {} specified in the config", + parent_.interface_name_.get()); + throw std::runtime_error( + "Invalid TX port '" + std::string(parent_.interface_name_.get()) + + "' specified in the config (port_id=" + std::to_string(port_id_) + ")"); + } else { + ANM_CONFIG_LOG("TX port {} found", port_id_); + } + + video_sampling_ = get_video_sampling_format(parent_.video_format_.get()); + color_bit_depth_ = get_color_bit_depth(parent_.bit_depth_.get()); + frame_size_ = calculate_frame_size(parent_.frame_width_.get(), parent_.frame_height_.get(), + video_sampling_, color_bit_depth_); + ANM_CONFIG_LOG("Expected frame size: {} bytes", frame_size_); + + expected_video_format_ = get_expected_gxf_video_format(video_sampling_, color_bit_depth_); + + ANM_LOG_INFO("AdvNetworkMediaTxOp::initialize() complete"); + } catch (const std::exception& e) { + ANM_LOG_ERROR("Error in AdvNetworkMediaTxOp initialization: {}", e.what()); + throw; + } + } + + /** + * @brief Creates a MediaFrame from a GXF entity containing a VideoBuffer. + * + * @param entity The GXF entity containing the video buffer. + * @return A shared pointer to the created MediaFrame, or nullptr if validation fails. + */ + std::shared_ptr create_media_frame_from_video_buffer(nvidia::gxf::Entity entity) { + try { + auto frame = std::make_unique(std::move(entity)); + auto result = frame->validate_frame_parameters(parent_.frame_width_.get(), + parent_.frame_height_.get(), frame_size_, expected_video_format_); + + if (result != Status::SUCCESS) { + ANM_LOG_ERROR("Video buffer validation failed"); + return nullptr; + } + + return std::make_shared(std::move(frame)); + } catch (const std::exception& e) { + ANM_LOG_ERROR("Video buffer error: {}", e.what()); + return nullptr; + } + } + + /** + * @brief Creates a MediaFrame from a GXF entity containing a Tensor. + * + * @param entity The GXF entity containing the tensor. + * @return A shared pointer to the created MediaFrame, or nullptr if validation fails. + */ + std::shared_ptr create_media_frame_from_tensor(nvidia::gxf::Entity entity) { + try { + auto frame = std::make_unique(std::move(entity), expected_video_format_); + auto result = frame->validate_frame_parameters(parent_.frame_width_.get(), + parent_.frame_height_.get(), frame_size_, expected_video_format_); + + if (result != Status::SUCCESS) { + ANM_LOG_ERROR("Tensor validation failed"); + return nullptr; + } + + return std::make_shared(std::move(frame)); + } catch (const std::exception& e) { + ANM_LOG_ERROR("Tensor error: {}", e.what()); + return nullptr; + } + } + + /** + * @brief Processes input data from the operator context. + * + * Extracts the GXF entity from the input and creates a MediaFrame for transmission. + * + * @param op_input The operator input context. + */ + void process_input(InputContext& op_input) { + static int retry_attempts = 0; // Count how many cycles frame has been pending + static int dropped_frames = 0; + + // Check if we still have a pending frame from previous call + if (pending_tx_frame_) { + retry_attempts++; + + // Check if we've exceeded maximum retry attempts - drop frame to prevent stall + if (retry_attempts >= MAX_RETRY_ATTEMPTS_BEFORE_DROP) { + dropped_frames++; + ANM_LOG_ERROR( + "TX port {}, queue {} exceeded max retry attempts ({}). Dropping frame to prevent " + "pipeline stall. Total dropped: {}", + port_id_, + parent_.queue_id_.get(), + retry_attempts, + dropped_frames); + pending_tx_frame_ = nullptr; // Drop the stuck frame + retry_attempts = 0; + // Fall through to receive new frame + } else { + // Frame still pending, skip new input this cycle and let process_output try again + ANM_STATS_TRACE( + "TX queue {} on port {} still has pending frame (retry_attempts: {}); " + "skipping new input.", + parent_.queue_id_.get(), port_id_, retry_attempts); + return; + } + } + + // No pending frame (or just dropped one), receive new input + auto maybe_entity = op_input.receive("input"); + if (!maybe_entity) return; + + auto& entity = static_cast(maybe_entity.value()); + + auto maybe_video_buffer = entity.get(); + if (maybe_video_buffer) { + pending_tx_frame_ = create_media_frame_from_video_buffer(std::move(entity)); + } else { + auto maybe_tensor = entity.get(); + if (!maybe_tensor) { + ANM_LOG_ERROR("Neither VideoBuffer nor Tensor found in message"); + return; + } + pending_tx_frame_ = create_media_frame_from_tensor(std::move(entity)); + } + + if (!pending_tx_frame_) { + ANM_LOG_ERROR("Failed to create media frame"); + return; + } + + // New frame received, reset retry counter + retry_attempts = 0; + } + + /** + * @brief Processes output data for the operator context. + * + * Transmits the pending media frame over the network if available. + * + * @param op_output The operator output context. + */ + void process_output(OutputContext& op_output) { + static int not_available_count = 0; + static int sent = 0; + static int err = 0; + + if (!pending_tx_frame_) { + ANM_LOG_ERROR("No pending TX frame"); + return; + } + + if (!cur_msg_) { + cur_msg_ = ano::create_tx_burst_params(); + ano::set_header(cur_msg_, port_id_, parent_.queue_id_.get(), 1, 1); + } + + if (!ano::is_tx_burst_available(cur_msg_)) { + std::this_thread::sleep_for(std::chrono::microseconds(SLEEP_WHEN_BURST_NOT_AVAILABLE_US)); + if (++not_available_count == DISPLAY_WARNING_AFTER_BURST_NOT_AVAILABLE) { + ANM_LOG_ERROR( + "TX port {}, queue {}, burst not available too many times consecutively. " + "Make sure memory region has enough buffers. Sent {} and error {}", + port_id_, + parent_.queue_id_.get(), + sent, + err); + not_available_count = 0; + err++; + } + return; + } + not_available_count = 0; + Status ret; + if ((ret = ano::get_tx_packet_burst(cur_msg_)) != Status::SUCCESS) { + ANM_LOG_ERROR("Error returned from get_tx_packet_burst: {}", static_cast(ret)); + return; + } + + cur_msg_->custom_pkt_data = std::move(pending_tx_frame_); + pending_tx_frame_ = nullptr; + + ret = ano::send_tx_burst(cur_msg_); + if (ret != Status::SUCCESS) { + ANM_LOG_ERROR("Error returned from send_tx_burst: {}", static_cast(ret)); + ano::free_tx_burst(cur_msg_); + err++; + } else { + sent++; + } + cur_msg_ = nullptr; + ANM_STATS_TRACE("AdvNetworkMediaTxOp::process_output() {}:{} done. Emitted{}/Error{}", + port_id_, + parent_.queue_id_.get(), + sent, + err); + } + + BurstParams* cur_msg_ = nullptr; + std::shared_ptr pending_tx_frame_ = nullptr; + size_t frame_size_; + nvidia::gxf::VideoFormat expected_video_format_; + int port_id_; + VideoFormatSampling video_sampling_; + VideoColorBitDepth color_bit_depth_; + + private: + AdvNetworkMediaTxOp& parent_; +}; + +AdvNetworkMediaTxOp::AdvNetworkMediaTxOp() : pimpl_(nullptr) { +} + +AdvNetworkMediaTxOp::~AdvNetworkMediaTxOp() { + if (pimpl_) { + delete pimpl_; + pimpl_ = nullptr; + } +} + +void AdvNetworkMediaTxOp::initialize() { + ANM_LOG_INFO("AdvNetworkMediaTxOp::initialize()"); + holoscan::Operator::initialize(); + + if (!pimpl_) { + pimpl_ = new AdvNetworkMediaTxOpImpl(*this); + } + + pimpl_->initialize(); +} + +void AdvNetworkMediaTxOp::setup(OperatorSpec& spec) { + spec.input("input"); + spec.param(interface_name_, + "interface_name", + "Name of NIC from advanced_network config", + "Name of NIC from advanced_network config"); + spec.param(queue_id_, "queue_id", "Queue ID", "Queue ID", default_queue_id); + spec.param(frame_width_, "frame_width", "Frame width", "Width of the frame", 1920); + spec.param( + frame_height_, "frame_height", "Frame height", "Height of the frame", 1080); + spec.param(bit_depth_, "bit_depth", "Bit depth", "Number of bits per pixel", 8); + spec.param( + video_format_, "video_format", "Video Format", "Video sample format", std::string("RGB888")); +} + +void AdvNetworkMediaTxOp::compute(InputContext& op_input, OutputContext& op_output, + ExecutionContext& context) { + pimpl_->process_input(op_input); + pimpl_->process_output(op_output); +} + +} // namespace holoscan::ops diff --git a/operators/advanced_network_media/advanced_network_media_tx/adv_network_media_tx.h b/operators/advanced_network_media/advanced_network_media_tx/adv_network_media_tx.h new file mode 100644 index 0000000000..032a51bccb --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_tx/adv_network_media_tx.h @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OPERATORS_ADVANCED_NETWORK_MEDIA_TX_ADV_NETWORK_MEDIA_TX_H_ +#define OPERATORS_ADVANCED_NETWORK_MEDIA_TX_ADV_NETWORK_MEDIA_TX_H_ + +#include +#include + +namespace holoscan::ops { + +// Forward declare the implementation class +class AdvNetworkMediaTxOpImpl; + +/** + * @class AdvNetworkMediaTxOp + * @brief Operator for transmitting media frames over advanced network infrastructure. + * + * This operator processes video frames from GXF entities (either VideoBuffer or Tensor) + * and transmits them over Rivermax-enabled network infrastructure. + */ +class AdvNetworkMediaTxOp : public Operator { + public: + static constexpr uint16_t default_queue_id = 0; + + HOLOSCAN_OPERATOR_FORWARD_ARGS(AdvNetworkMediaTxOp) + + /** + * @brief Constructs an AdvNetworkMediaTxOp operator. + */ + AdvNetworkMediaTxOp(); + + /** + * @brief Destroys the AdvNetworkMediaTxOp operator and its implementation. + */ + ~AdvNetworkMediaTxOp(); + + void initialize() override; + void setup(OperatorSpec& spec) override; + void compute(InputContext& op_input, OutputContext& op_output, ExecutionContext&) override; + + private: + friend class AdvNetworkMediaTxOpImpl; + + // Parameters + Parameter interface_name_; + Parameter queue_id_; + Parameter video_format_; + Parameter bit_depth_; + Parameter frame_width_; + Parameter frame_height_; + + AdvNetworkMediaTxOpImpl* pimpl_; +}; + +} // namespace holoscan::ops + +#endif // OPERATORS_ADVANCED_NETWORK_MEDIA_TX_ADV_NETWORK_MEDIA_TX_H_ diff --git a/operators/advanced_network_media/advanced_network_media_tx/metadata.json b/operators/advanced_network_media/advanced_network_media_tx/metadata.json new file mode 100644 index 0000000000..3e7d6f6b9d --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_tx/metadata.json @@ -0,0 +1,32 @@ +{ + "operator": { + "name": "advanced_network_media_tx", + "authors": [ + { + "name": "Rony Rado", + "affiliation": "NVIDIA" + } + ], + "version": "1.0", + "changelog": { + "1.0": "Initial Release" + }, + "language": ["C++", "Python"], + "platforms": ["x86_64", "aarch64"], + "tags": ["Networking and Distributed Computing", "DPDK", "UDP", "Ethernet", "IP", "GPUDirect", "Rivermax"], + "holoscan_sdk": { + "minimum_required_version": "2.6.0", + "tested_versions": [ + "2.6.0" + ] + }, + "ranking": 3, + "requirements": { + "operators": [{ + "name": "advanced_network", + "version": "1.4" + } + ] + } + } +} \ No newline at end of file diff --git a/operators/advanced_network_media/advanced_network_media_tx/python/CMakeLists.txt b/operators/advanced_network_media/advanced_network_media_tx/python/CMakeLists.txt new file mode 100644 index 0000000000..c012fa5e3e --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_tx/python/CMakeLists.txt @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +include(pybind11_add_holohub_module) +find_package(holoscan 2.6 REQUIRED CONFIG + PATHS "/opt/nvidia/holoscan" "/workspace/holoscan-sdk/install") + +set(MODULE_NAME advanced_network_media_tx) +set(MODULE_CLASS_NAME "*") + +pybind11_add_holohub_module( + CPP_CMAKE_TARGET advanced_network_media_tx + CLASS_NAME "AdvNetworkMediaTxOp" + SOURCES adv_network_media_tx_pybind.cpp +) + +target_link_libraries(${MODULE_NAME}_python +PRIVATE + holoscan::core + ${MODULE_NAME} +) + +set(CMAKE_PYBIND11_HOLOHUB_MODULE_OUT_DIR ${CMAKE_BINARY_DIR}/python/${CMAKE_INSTALL_LIBDIR}/holohub) +set(CMAKE_SUBMODULE_OUT_DIR ${CMAKE_PYBIND11_HOLOHUB_MODULE_OUT_DIR}/${MODULE_NAME}) + +set_target_properties(${MODULE_NAME}_python PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SUBMODULE_OUT_DIR} + OUTPUT_NAME _${MODULE_NAME} +) + +configure_file( + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/pybind11/__init__.py + ${CMAKE_PYBIND11_HOLOHUB_MODULE_OUT_DIR}/advanced_network_media_tx/__init__.py +) diff --git a/operators/advanced_network_media/advanced_network_media_tx/python/adv_network_media_tx_pybind.cpp b/operators/advanced_network_media/advanced_network_media_tx/python/adv_network_media_tx_pybind.cpp new file mode 100644 index 0000000000..857ac4b3e7 --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_tx/python/adv_network_media_tx_pybind.cpp @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "../adv_network_media_tx.h" +#include "./adv_network_media_tx_pydoc.hpp" + +#include +#include // for unordered_map -> dict, etc. + +#include +#include +#include + +#include "../../../operator_util.hpp" +#include +#include +#include +#include + +using std::string_literals::operator""s; +using pybind11::literals::operator""_a; + +#define STRINGIFY(x) #x +#define MACRO_STRINGIFY(x) STRINGIFY(x) + +namespace py = pybind11; + +namespace holoscan::ops { + +/* Trampoline classes for handling Python kwargs + * + * These add a constructor that takes a Fragment for which to initialize the operator. + * The explicit parameter list and default arguments take care of providing a Pythonic + * kwarg-based interface with appropriate default values matching the operator's + * default parameters in the C++ API `setup` method. + * + * The sequence of events in this constructor is based on Fragment::make_operator + */ + +class PyAdvNetworkMediaTxOp : public AdvNetworkMediaTxOp { + public: + /* Inherit the constructors */ + using AdvNetworkMediaTxOp::AdvNetworkMediaTxOp; + + // Define a constructor that fully initializes the object. + PyAdvNetworkMediaTxOp(Fragment* fragment, const py::args& args, + const std::string& interface_name = "", + uint16_t queue_id = default_queue_id, + const std::string& video_format = "RGB888", uint32_t bit_depth = 8, + uint32_t frame_width = 1920, uint32_t frame_height = 1080, + const std::string& name = "advanced_network_media_tx") { + add_positional_condition_and_resource_args(this, args); + name_ = name; + fragment_ = fragment; + spec_ = std::make_shared(fragment); + setup(*spec_.get()); + + // Set parameters if provided + if (!interface_name.empty()) { this->add_arg(Arg("interface_name", interface_name)); } + this->add_arg(Arg("queue_id", queue_id)); + this->add_arg(Arg("video_format", video_format)); + this->add_arg(Arg("bit_depth", bit_depth)); + this->add_arg(Arg("frame_width", frame_width)); + this->add_arg(Arg("frame_height", frame_height)); + } +}; + +PYBIND11_MODULE(_advanced_network_media_tx, m) { + m.doc() = R"pbdoc( + Holoscan SDK Advanced Networking Media TX Operator Python Bindings + ------------------------------------------------------------------ + .. currentmodule:: _advanced_network_media_tx + + This module provides Python bindings for the Advanced Networking Media TX operator, + which transmits video frames over Rivermax-enabled network infrastructure. + )pbdoc"; + +#ifdef VERSION_INFO + m.attr("__version__") = MACRO_STRINGIFY(VERSION_INFO); +#else + m.attr("__version__") = "dev"; +#endif + + py::class_>( + m, "AdvNetworkMediaTxOp", doc::AdvNetworkMediaTxOp::doc_AdvNetworkMediaTxOp) + .def(py::init(), + "fragment"_a, + "interface_name"_a = ""s, + "queue_id"_a = AdvNetworkMediaTxOp::default_queue_id, + "video_format"_a = "RGB888"s, + "bit_depth"_a = 8, + "frame_width"_a = 1920, + "frame_height"_a = 1080, + "name"_a = "advanced_network_media_tx"s, + doc::AdvNetworkMediaTxOp::doc_AdvNetworkMediaTxOp_python) + .def("initialize", &AdvNetworkMediaTxOp::initialize, doc::AdvNetworkMediaTxOp::doc_initialize) + .def("setup", &AdvNetworkMediaTxOp::setup, "spec"_a, doc::AdvNetworkMediaTxOp::doc_setup); +} // PYBIND11_MODULE NOLINT +} // namespace holoscan::ops diff --git a/operators/advanced_network_media/advanced_network_media_tx/python/adv_network_media_tx_pydoc.hpp b/operators/advanced_network_media/advanced_network_media_tx/python/adv_network_media_tx_pydoc.hpp new file mode 100644 index 0000000000..5a464a6c2d --- /dev/null +++ b/operators/advanced_network_media/advanced_network_media_tx/python/adv_network_media_tx_pydoc.hpp @@ -0,0 +1,92 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef PYHOLOHUB_OPERATORS_ADV_NET_MEDIA_TX_PYDOC_HPP +#define PYHOLOHUB_OPERATORS_ADV_NET_MEDIA_TX_PYDOC_HPP + +#include + +#include "macros.hpp" + +namespace holoscan::doc { + +namespace AdvNetworkMediaTxOp { + +PYDOC(AdvNetworkMediaTxOp, R"doc( +Advanced Networking Media Transmitter operator. + +This operator processes video frames from GXF entities (either VideoBuffer or Tensor) +and transmits them over Rivermax-enabled network infrastructure. +)doc") + +// PyAdvNetworkMediaTxOp Constructor +PYDOC(AdvNetworkMediaTxOp_python, R"doc( +Advanced Networking Media Transmitter operator. + +This operator processes video frames from GXF entities (either VideoBuffer or Tensor) +and transmits them over Rivermax-enabled network infrastructure. + +Parameters +---------- +fragment : Fragment + The fragment that the operator belongs to. +interface_name : str, optional + Name of the network interface to use for transmission. +queue_id : int, optional + Queue ID for the network interface (default: 0). +video_format : str, optional + Video format for transmission (default: "RGB888"). +bit_depth : int, optional + Bit depth of the video data (default: 8). +frame_width : int, optional + Width of the video frame in pixels (default: 1920). +frame_height : int, optional + Height of the video frame in pixels (default: 1080). +name : str, optional + The name of the operator (default: "advanced_network_media_tx"). +)doc") + +PYDOC(gxf_typename, R"doc( +The GXF type name of the resource. + +Returns +------- +str + The GXF type name of the resource +)doc") + +PYDOC(initialize, R"doc( +Initialize the operator. + +This method is called only once when the operator is created for the first time, +and uses a light-weight initialization. +)doc") + +PYDOC(setup, R"doc( +Define the operator specification. + +Parameters +---------- +spec : ``holoscan.core.OperatorSpec`` + The operator specification. +)doc") + +} // namespace AdvNetworkMediaTxOp + +} // namespace holoscan::doc + +#endif // PYHOLOHUB_OPERATORS_ADV_NET_MEDIA_TX_PYDOC_HPP diff --git a/operators/advanced_network_media/common/CMakeLists.txt b/operators/advanced_network_media/common/CMakeLists.txt new file mode 100644 index 0000000000..3722c0b13f --- /dev/null +++ b/operators/advanced_network_media/common/CMakeLists.txt @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +cmake_minimum_required(VERSION 3.20) +project(advanced_network_media_common) + +add_library(${PROJECT_NAME} STATIC + video_parameters.cpp + frame_buffer.cpp +) + +# Add Position Independent Code flag for static library to be linked into shared libraries +set_property(TARGET ${PROJECT_NAME} PROPERTY POSITION_INDEPENDENT_CODE ON) + +target_include_directories(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../../advanced_network + ${CMAKE_CURRENT_SOURCE_DIR}/../../advanced_network/advanced_network +) + +target_link_libraries(${PROJECT_NAME} + PRIVATE + holoscan::core + GXF::multimedia + rivermax-dev-kit +) diff --git a/operators/advanced_network_media/common/adv_network_media_common.h b/operators/advanced_network_media/common/adv_network_media_common.h new file mode 100644 index 0000000000..d8455a10f0 --- /dev/null +++ b/operators/advanced_network_media/common/adv_network_media_common.h @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OPERATORS_ADVANCED_NETWORK_MEDIA_COMMON_ADV_NETWORK_MEDIA_COMMON_H_ +#define OPERATORS_ADVANCED_NETWORK_MEDIA_COMMON_ADV_NETWORK_MEDIA_COMMON_H_ + +#include +#include +#include +#include "rtp_params.h" +#include "adv_network_media_logging.h" + +#define CUDA_TRY(stmt) \ + { \ + cudaError_t cuda_status = stmt; \ + if (cudaSuccess != cuda_status) { \ + ANM_LOG_ERROR("Runtime call {} in line {} of file {} failed with '{}' ({})", \ + #stmt, \ + __LINE__, \ + __FILE__, \ + cudaGetErrorString(cuda_status), \ + static_cast(cuda_status)); \ + throw std::runtime_error("CUDA operation failed"); \ + } \ + } + +#endif // OPERATORS_ADVANCED_NETWORK_MEDIA_COMMON_ADV_NETWORK_MEDIA_COMMON_H_ diff --git a/operators/advanced_network_media/common/adv_network_media_logging.h b/operators/advanced_network_media/common/adv_network_media_logging.h new file mode 100644 index 0000000000..6232943a4a --- /dev/null +++ b/operators/advanced_network_media/common/adv_network_media_logging.h @@ -0,0 +1,232 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Advanced Network Media Operator - Unified Logging Control System + * + * This header provides centralized control over all logging in the advanced_network_media operator. + * Different logging categories can be independently enabled/disabled for performance optimization. + */ + +#ifndef OPERATORS_ADVANCED_NETWORK_MEDIA_COMMON_ADV_NETWORK_MEDIA_LOGGING_H_ +#define OPERATORS_ADVANCED_NETWORK_MEDIA_COMMON_ADV_NETWORK_MEDIA_LOGGING_H_ + +#include + +// ======================================================================================== +// LOGGING CONTROL CONFIGURATION +// ======================================================================================== +// +// The advanced network media operator supports multiple logging levels for different +// use cases and performance requirements: +// +// 1. CRITICAL LOGS (Always Enabled): +// - Errors that indicate operator malfunction +// - Warnings about configuration issues or data problems +// - Essential initialization/configuration messages +// +// 2. PACKET/POINTER TRACING (ENABLE_PACKET_TRACING): +// - Per-packet processing details +// - Memory pointer tracking and validation +// - Low-level RTP packet analysis +// - Buffer management operations +// - PERFORMANCE IMPACT: High (5-15% overhead) +// +// 3. STATISTICS AND MONITORING (ENABLE_STATISTICS_LOGGING): +// - Frame completion statistics +// - Throughput and performance metrics +// - Periodic statistics reports +// - Enhanced error tracking and analytics +// - PERFORMANCE IMPACT: Medium (1-5% overhead) +// +// 4. CONFIGURATION AND STATE (ENABLE_CONFIG_LOGGING): +// - Strategy detection results +// - State machine transitions +// - Memory copy strategy configuration +// - Burst parameter updates +// - PERFORMANCE IMPACT: Low (0.1-1% overhead) +// +// PRODUCTION DEPLOYMENT RECOMMENDATIONS: +// - Comment out all ENABLE_* flags for maximum performance +// - Keep only critical error/warning logs +// - Use for high-throughput production environments +// +// DEVELOPMENT/DEBUGGING RECOMMENDATIONS: +// - Frame assembly issues: Enable ENABLE_FRAME_TRACING + ENABLE_MEMCOPY_TRACING +// - Network/frame processing: Enable ENABLE_FRAME_TRACING + ENABLE_PACKET_TRACING +// - Strategy detection problems: Enable ENABLE_STRATEGY_TRACING + ENABLE_MEMCOPY_TRACING +// - State machine issues: Enable ENABLE_STATE_LOGGING + ENABLE_FRAME_TRACING +// - Deep state debugging: Enable ENABLE_STATE_TRACING + ENABLE_STATE_LOGGING +// - Configuration issues: Enable ENABLE_CONFIG_LOGGING +// - Performance analysis: Enable ENABLE_STATISTICS_LOGGING + ENABLE_FRAME_TRACING +// +// TROUBLESHOOTING SPECIFIC ISSUES: +// - Packet loss/corruption: ENABLE_PACKET_TRACING + ENABLE_FRAME_TRACING +// - Frame drop/corruption: ENABLE_FRAME_TRACING + ENABLE_MEMCOPY_TRACING +// - Memory copy errors: ENABLE_MEMCOPY_TRACING only +// - Strategy detection failures: ENABLE_STRATEGY_TRACING only +// - Strategy vs execution: ENABLE_STRATEGY_TRACING + ENABLE_MEMCOPY_TRACING +// - State transitions/recovery: ENABLE_STATE_LOGGING only +// - State validation/internals: ENABLE_STATE_TRACING only +// - Complex state issues: ENABLE_STATE_LOGGING + ENABLE_STATE_TRACING +// - Setup/parameter issues: ENABLE_CONFIG_LOGGING only +// - Performance bottlenecks: ENABLE_STATISTICS_LOGGING + ENABLE_FRAME_TRACING +// + +// ======================================================================================== +// LOGGING LEVEL CONTROLS (0 = disabled, 1 = enabled) +// ======================================================================================== + +// Packet-level tracing (highest performance impact) +// Includes: individual packet processing, pointer tracking, buffer operations +#define ENABLE_PACKET_TRACING 0 + +// Frame-level tracing (medium-high performance impact) +// Includes: frame allocation, emission, completion, lifecycle events +#define ENABLE_FRAME_TRACING 0 + +// Memory copy tracing (medium performance impact) +// Includes: packet-to-frame assembly, memory copy strategy operations +#define ENABLE_MEMCOPY_TRACING 0 + +// Strategy detection tracing (low-medium performance impact) +// Includes: memory copy strategy detection, pattern analysis, strategy switching +#define ENABLE_STRATEGY_TRACING 1 + +// State machine logging/tracing (medium performance impact) +// Includes: state transitions, state validation, internal state debugging, error recovery +#define ENABLE_STATE_LOGGING 0 + +// Statistics and monitoring (medium performance impact) +// Includes: frame completion stats, throughput metrics, periodic reports +#define ENABLE_STATISTICS_LOGGING 1 + +// Verbose statistics logging (high performance impact) +// Includes: per-frame detailed logging (very verbose - use only for debugging) +#define ENABLE_VERBOSE_STATISTICS 0 + +// Performance logging (medium-high performance impact) +// Includes: timing measurements, performance profiling, compute duration tracking +#define ENABLE_PERFORMANCE_LOGGING 0 + +// Configuration logging (low performance impact) +// Includes: parameter updates, operator setup, initialization +#define ENABLE_CONFIG_LOGGING 1 + +// ======================================================================================== +// LOGGING MACRO DEFINITIONS +// ======================================================================================== + +// Critical logs - ALWAYS ENABLED (errors, warnings, essential info) +#define ANM_LOG_ERROR(fmt, ...) HOLOSCAN_LOG_ERROR("[ANM] " fmt, ##__VA_ARGS__) +#define ANM_LOG_WARN(fmt, ...) HOLOSCAN_LOG_WARN("[ANM] " fmt, ##__VA_ARGS__) +#define ANM_LOG_CRITICAL(fmt, ...) HOLOSCAN_LOG_CRITICAL("[ANM] " fmt, ##__VA_ARGS__) +#define ANM_LOG_INFO(fmt, ...) HOLOSCAN_LOG_INFO("[ANM] " fmt, ##__VA_ARGS__) + +// Packet/Pointer tracing - CONDITIONAL (per-packet details, pointer tracking) +#if ENABLE_PACKET_TRACING +#define ANM_PACKET_TRACE(fmt, ...) HOLOSCAN_LOG_INFO("[ANM_PACKET] " fmt, ##__VA_ARGS__) +#define ANM_POINTER_TRACE(fmt, ...) HOLOSCAN_LOG_INFO("[ANM_PTR] " fmt, ##__VA_ARGS__) +#else +#define ANM_PACKET_TRACE(fmt, ...) \ + do { \ + } while (0) +#define ANM_POINTER_TRACE(fmt, ...) \ + do { \ + } while (0) +#endif + +// Statistics and monitoring - CONDITIONAL (frame stats, metrics) +#if ENABLE_STATISTICS_LOGGING +#define ANM_STATS_LOG(fmt, ...) HOLOSCAN_LOG_INFO("[ANM_STATS] " fmt, ##__VA_ARGS__) +#define ANM_STATS_UPDATE(code) \ + do { code; } while (0) +#else +#define ANM_STATS_LOG(fmt, ...) \ + do { \ + } while (0) +#define ANM_STATS_UPDATE(code) \ + do { \ + } while (0) +#endif + +// Statistics tracing - CONDITIONAL (per-frame detailed logging) +#if ENABLE_VERBOSE_STATISTICS +#define ANM_STATS_TRACE(fmt, ...) HOLOSCAN_LOG_INFO("[ANM_STATS] " fmt, ##__VA_ARGS__) +#else +#define ANM_STATS_TRACE(fmt, ...) \ + do { \ + } while (0) +#endif + +// Performance logging - CONDITIONAL (timing measurements, profiling) +#if ENABLE_PERFORMANCE_LOGGING +#define ANM_PERF_LOG(fmt, ...) HOLOSCAN_LOG_INFO("[ANM_PERF] " fmt, ##__VA_ARGS__) +#else +#define ANM_PERF_LOG(fmt, ...) \ + do { \ + } while (0) +#endif + +// Configuration logging - CONDITIONAL (operator setup, parameters) +#if ENABLE_CONFIG_LOGGING +#define ANM_CONFIG_LOG(fmt, ...) HOLOSCAN_LOG_INFO("[ANM_CONFIG] " fmt, ##__VA_ARGS__) +#else +#define ANM_CONFIG_LOG(fmt, ...) \ + do { \ + } while (0) +#endif + +// State machine logging/tracing - CONDITIONAL (state transitions, validation, debugging) +#if ENABLE_STATE_LOGGING +#define ANM_STATE_LOG(fmt, ...) HOLOSCAN_LOG_INFO("[ANM_STATE] " fmt, ##__VA_ARGS__) +#define ANM_STATE_TRACE(fmt, ...) HOLOSCAN_LOG_INFO("[ANM_STATE] " fmt, ##__VA_ARGS__) +#else +#define ANM_STATE_LOG(fmt, ...) \ + do { \ + } while (0) +#define ANM_STATE_TRACE(fmt, ...) \ + do { \ + } while (0) +#endif + +// ======================================================================================== +// SPECIALIZED LOGGING HELPERS +// ======================================================================================== + +// Error with frame context (always enabled for critical errors) +#define ANM_FRAME_ERROR(frame_num, fmt, ...) \ + ANM_LOG_ERROR("Frame {} - " fmt, frame_num, ##__VA_ARGS__) + +// Warning with frame context (always enabled) +#define ANM_FRAME_WARN(frame_num, fmt, ...) \ + ANM_LOG_WARN("Frame {} - " fmt, frame_num, ##__VA_ARGS__) + +// Frame-level tracing (independent control) +#if ENABLE_FRAME_TRACING +#define ANM_FRAME_TRACE(fmt, ...) HOLOSCAN_LOG_INFO("[ANM_FRAME] " fmt, ##__VA_ARGS__) +#else +#define ANM_FRAME_TRACE(fmt, ...) \ + do { \ + } while (0) +#endif + +// Memory copy operation tracing (independent control) +#if ENABLE_MEMCOPY_TRACING +#define ANM_MEMCOPY_TRACE(fmt, ...) HOLOSCAN_LOG_INFO("[ANM_MEMCOPY] " fmt, ##__VA_ARGS__) +#else +#define ANM_MEMCOPY_TRACE(fmt, ...) \ + do { \ + } while (0) +#endif + +// Strategy detection tracing (independent control) +#if ENABLE_STRATEGY_TRACING +#define ANM_STRATEGY_LOG(fmt, ...) HOLOSCAN_LOG_INFO("[ANM_STRATEGY] " fmt, ##__VA_ARGS__) +#else +#define ANM_STRATEGY_LOG(fmt, ...) \ + do { \ + } while (0) +#endif + +#endif // OPERATORS_ADVANCED_NETWORK_MEDIA_COMMON_ADV_NETWORK_MEDIA_LOGGING_H_ diff --git a/operators/advanced_network_media/common/frame_buffer.cpp b/operators/advanced_network_media/common/frame_buffer.cpp new file mode 100644 index 0000000000..40f8e5f19d --- /dev/null +++ b/operators/advanced_network_media/common/frame_buffer.cpp @@ -0,0 +1,274 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "frame_buffer.h" +#include "adv_network_media_logging.h" + +namespace holoscan::ops { + +Status VideoFrameBufferBase::validate_frame_parameters( + uint32_t expected_width, uint32_t expected_height, size_t expected_frame_size, + nvidia::gxf::VideoFormat expected_format) const { + if (width_ != expected_width || height_ != expected_height) { + ANM_LOG_ERROR( + "Resolution mismatch: {}x{} vs {}x{}", width_, height_, expected_width, expected_height); + return Status::INVALID_PARAMETER; + } + + if (frame_size_ != expected_frame_size) { + ANM_LOG_ERROR("Frame size mismatch: {} vs {}", frame_size_, expected_frame_size); + return Status::INVALID_PARAMETER; + } + + return validate_format_compliance(expected_format); +} + +VideoBufferFrameBuffer::VideoBufferFrameBuffer(nvidia::gxf::Entity entity) { + entity_ = std::move(entity); + + auto maybe_video_buffer = entity_.get(); + if (!maybe_video_buffer) throw std::runtime_error("Entity doesn't contain a video buffer"); + + buffer_ = maybe_video_buffer.value(); + const auto& info = buffer_->video_frame_info(); + width_ = info.width; + height_ = info.height; + src_storage_type_ = buffer_->storage_type(); + memory_location_ = from_gxf_memory_type(src_storage_type_); + frame_size_ = buffer_->size(); + format_ = info.color_format; + planes_ = info.color_planes; +} + +Status VideoBufferFrameBuffer::validate_format_compliance( + nvidia::gxf::VideoFormat expected_format) const { + if (expected_format == nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_NV12_709) { + if (format_ != nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_NV12_709 && + format_ != nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_YUV420_709) { + ANM_LOG_ERROR("Invalid NV12_709 format"); + return Status::INVALID_PARAMETER; + } + } else if (expected_format == nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_RGB) { + if (format_ != nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_RGB) { + ANM_LOG_ERROR("Invalid RGB format"); + return Status::INVALID_PARAMETER; + } + } else if (format_ != nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_CUSTOM) { + ANM_LOG_ERROR( + "Format mismatch: {} vs {}", static_cast(format_), static_cast(expected_format)); + return Status::INVALID_PARAMETER; + } + + if (expected_format == nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_NV12_709) { + if (width_ % SMPTE_420_ALIGNMENT != 0 || height_ % SMPTE_420_ALIGNMENT != 0) { + ANM_LOG_ERROR("Resolution not 4:2:0 aligned"); + return Status::INVALID_PARAMETER; + } + } + + for (const auto& plane : planes_) { + if (plane.stride % SMPTE_STRIDE_ALIGNMENT != 0) { + ANM_LOG_ERROR("Stride {} not {}-byte aligned", plane.stride, SMPTE_STRIDE_ALIGNMENT); + return Status::INVALID_PARAMETER; + } + } + + return Status::SUCCESS; +} + +TensorFrameBuffer::TensorFrameBuffer(nvidia::gxf::Entity entity, nvidia::gxf::VideoFormat format) { + entity_ = std::move(entity); + + auto maybe_tensor = entity_.get(); + if (!maybe_tensor) throw std::runtime_error("Entity doesn't contain a tensor"); + tensor_ = maybe_tensor.value(); + + const auto& shape = tensor_->shape(); + width_ = shape.dimension(1); + height_ = shape.dimension(0); + src_storage_type_ = tensor_->storage_type(); + memory_location_ = from_gxf_memory_type(src_storage_type_); + frame_size_ = tensor_->size(); + format_ = format; +} + +Status TensorFrameBuffer::validate_format_compliance( + nvidia::gxf::VideoFormat expected_format) const { + const auto& shape = tensor_->shape(); + switch (format_) { + case nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_NV12_709: + if (shape.rank() != 3 || shape.dimension(2) != 2 || + tensor_->element_type() != nvidia::gxf::PrimitiveType::kUnsigned8) { + ANM_LOG_ERROR("Invalid NV12_709 tensor"); + return Status::INVALID_PARAMETER; + } + break; + + case nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_RGB: + if (shape.rank() != 3 || shape.dimension(2) != 3) { + ANM_LOG_ERROR("Invalid RGB tensor"); + return Status::INVALID_PARAMETER; + } + break; + + default: + ANM_LOG_ERROR("Unsupported tensor format: {}", static_cast(format_)); + return Status::INVALID_PARAMETER; + } + return Status::SUCCESS; +} + +AllocatedVideoBufferFrameBuffer::AllocatedVideoBufferFrameBuffer( + void* data, size_t size, uint32_t width, uint32_t height, nvidia::gxf::VideoFormat format, + nvidia::gxf::MemoryStorageType storage_type) { + data_ = data; + frame_size_ = size; + width_ = width; + height_ = height; + format_ = format; + src_storage_type_ = storage_type; + memory_location_ = from_gxf_memory_type(storage_type); +} + +Status AllocatedVideoBufferFrameBuffer::validate_format_compliance( + nvidia::gxf::VideoFormat expected_format) const { + if (format_ != expected_format) { + ANM_LOG_ERROR( + "Format mismatch: {} vs {}", static_cast(format_), static_cast(expected_format)); + return Status::INVALID_PARAMETER; + } + return Status::SUCCESS; +} + +nvidia::gxf::Entity AllocatedVideoBufferFrameBuffer::wrap_in_entity( + void* context, std::function(void*)> release_func) { + auto result = nvidia::gxf::Entity::New(context); + if (!result) { throw std::runtime_error("Failed to allocate entity"); } + + auto buffer = result.value().add(); + if (!buffer) { throw std::runtime_error("Failed to allocate video buffer"); } + + // Set up video buffer based on format + nvidia::gxf::VideoBufferInfo info; + + switch (format_) { + case nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_RGB: { + nvidia::gxf::VideoTypeTraits video_type; + nvidia::gxf::VideoFormatSize color_format; + auto color_planes = color_format.getDefaultColorPlanes(width_, height_, false); + info = {width_, + height_, + video_type.value, + color_planes, + nvidia::gxf::SurfaceLayout::GXF_SURFACE_LAYOUT_PITCH_LINEAR}; + break; + } + case nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_NV12_709: { + nvidia::gxf::VideoTypeTraits video_type; + nvidia::gxf::VideoFormatSize + color_format; + auto color_planes = color_format.getDefaultColorPlanes(width_, height_, false); + info = {width_, + height_, + video_type.value, + color_planes, + nvidia::gxf::SurfaceLayout::GXF_SURFACE_LAYOUT_PITCH_LINEAR}; + break; + } + default: + throw std::runtime_error("Unsupported video format"); + } + + buffer.value()->wrapMemory(info, frame_size_, src_storage_type_, data_, release_func); + + return result.value(); +} + +AllocatedTensorFrameBuffer::AllocatedTensorFrameBuffer( + void* data, size_t size, uint32_t width, uint32_t height, uint32_t channels, + nvidia::gxf::VideoFormat format, nvidia::gxf::MemoryStorageType storage_type) { + data_ = data; + frame_size_ = size; + width_ = width; + height_ = height; + channels_ = channels; + format_ = format; + src_storage_type_ = storage_type; + memory_location_ = from_gxf_memory_type(storage_type); +} + +Status AllocatedTensorFrameBuffer::validate_format_compliance( + nvidia::gxf::VideoFormat expected_format) const { + if (format_ != expected_format) { + ANM_LOG_ERROR( + "Format mismatch: {} vs {}", static_cast(format_), static_cast(expected_format)); + return Status::INVALID_PARAMETER; + } + + // Validate channel count based on format + if (expected_format == nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_RGB && channels_ != 3) { + ANM_LOG_ERROR("Invalid channel count for RGB format: {}", channels_); + return Status::INVALID_PARAMETER; + } else if (expected_format == nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_NV12_709 && + channels_ != 2) { + ANM_LOG_ERROR("Invalid channel count for NV12_709 format: {}", channels_); + return Status::INVALID_PARAMETER; + } + + return Status::SUCCESS; +} + +nvidia::gxf::Entity AllocatedTensorFrameBuffer::wrap_in_entity( + void* context, std::function(void*)> release_func) { + auto result = nvidia::gxf::Entity::New(context); + if (!result) { throw std::runtime_error("Failed to allocate entity"); } + + auto tensor = result.value().add(); + if (!tensor) { throw std::runtime_error("Failed to allocate tensor"); } + + // Set up tensor shape based on format + nvidia::gxf::Shape shape; + auto element_type = nvidia::gxf::PrimitiveType::kUnsigned8; + + switch (format_) { + case nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_RGB: + shape = {static_cast(height_), static_cast(width_), 3}; + break; + case nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_NV12_709: + // For NV12, we need 2 channels (Y and UV interleaved) + shape = {static_cast(height_), static_cast(width_), 2}; + break; + case nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_CUSTOM: + // For custom format, use the channels_ value provided + shape = {static_cast(height_), + static_cast(width_), + static_cast(channels_)}; + break; + default: + throw std::runtime_error("Unsupported tensor format"); + } + + auto element_size = nvidia::gxf::PrimitiveTypeSize(element_type); + auto strides = nvidia::gxf::ComputeTrivialStrides(shape, element_size); + + tensor.value()->wrapMemory( + shape, element_type, element_size, strides, src_storage_type_, data_, release_func); + + return result.value(); +} + +} // namespace holoscan::ops diff --git a/operators/advanced_network_media/common/frame_buffer.h b/operators/advanced_network_media/common/frame_buffer.h new file mode 100644 index 0000000000..597ab09073 --- /dev/null +++ b/operators/advanced_network_media/common/frame_buffer.h @@ -0,0 +1,254 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef OPERATORS_ADVANCED_NETWORK_MEDIA_COMMON_FRAME_BUFFER_H_ +#define OPERATORS_ADVANCED_NETWORK_MEDIA_COMMON_FRAME_BUFFER_H_ + +#include +#include +#include + +#include "rdk/services/services.h" +#include "advanced_network/common.h" +#include "gxf/multimedia/video.hpp" +#include "gxf/core/entity.hpp" +#include "gxf/core/expected.hpp" +#include +#include "video_parameters.h" + +namespace holoscan::ops { + +using rivermax::dev_kit::services::IFrameBuffer; +using rivermax::dev_kit::services::MemoryLocation; +using rivermax::dev_kit::services::byte_t; +using holoscan::advanced_network::Status; + +/** + * @class FrameBufferBase + * @brief Base class for frame buffers used in media transmission operations. + */ +class FrameBufferBase : public IFrameBuffer { + public: + virtual ~FrameBufferBase() = default; + + size_t get_size() const override { return frame_size_; } + size_t get_aligned_size() const override { return frame_size_; } + MemoryLocation get_memory_location() const override { return memory_location_; } + + protected: + /** + * @brief Converts GXF memory storage type to MemoryLocation enum. + * + * @param storage_type The GXF memory storage type to convert. + * @return The corresponding MemoryLocation. + */ + inline MemoryLocation from_gxf_memory_type(nvidia::gxf::MemoryStorageType storage_type) const { + switch (storage_type) { + case nvidia::gxf::MemoryStorageType::kHost: + case nvidia::gxf::MemoryStorageType::kSystem: + return MemoryLocation::Host; + case nvidia::gxf::MemoryStorageType::kDevice: + return MemoryLocation::GPU; + default: + return MemoryLocation::Host; + } + } + + protected: + MemoryLocation memory_location_; + nvidia::gxf::MemoryStorageType src_storage_type_; + size_t frame_size_; + nvidia::gxf::Entity entity_; +}; + +/** + * @class VideoFrameBufferBase + * @brief Base class for video frame buffers with common validation functionality. + */ +class VideoFrameBufferBase : public FrameBufferBase { + public: + virtual ~VideoFrameBufferBase() = default; + + /** + * @brief Validates the frame buffer against expected parameters. + * + * @param expected_width Expected frame width. + * @param expected_height Expected frame height. + * @param expected_frame_size Expected frame size in bytes. + * @param expected_format Expected video format. + * @return Status indicating validation result. + */ + Status validate_frame_parameters(uint32_t expected_width, uint32_t expected_height, + size_t expected_frame_size, + nvidia::gxf::VideoFormat expected_format) const; + + protected: + /** + * @brief Implementation-specific validation logic to be defined by derived classes. + * + * @param fmt The expected video format. + * @return Status indicating validation result. + */ + virtual Status validate_format_compliance(nvidia::gxf::VideoFormat fmt) const = 0; + + protected: + static constexpr uint32_t SMPTE_STRIDE_ALIGNMENT = 256; + static constexpr uint32_t SMPTE_420_ALIGNMENT = 2; + + uint32_t width_; + uint32_t height_; + nvidia::gxf::VideoFormat format_; +}; + +/** + * @class VideoBufferFrameBuffer + * @brief Frame buffer implementation for VideoBuffer type frames. + */ +class VideoBufferFrameBuffer : public VideoFrameBufferBase { + public: + /** + * @brief Constructs a VideoBufferFrameBuffer from a GXF entity. + * + * @param entity The GXF entity containing the video buffer. + */ + explicit VideoBufferFrameBuffer(nvidia::gxf::Entity entity); + byte_t* get() const override { + return (buffer_) ? static_cast(buffer_->pointer()) : nullptr; + } + + protected: + Status validate_format_compliance(nvidia::gxf::VideoFormat fmt) const override; + + private: + nvidia::gxf::Handle buffer_; + std::vector planes_; +}; + +/** + * @class TensorFrameBuffer + * @brief Frame buffer implementation for Tensor type frames. + */ +class TensorFrameBuffer : public VideoFrameBufferBase { + public: + /** + * @brief Constructs a TensorFrameBuffer from a GXF entity with a specific format. + * + * @param entity The GXF entity containing the tensor. + * @param format The video format to interpret the tensor as. + */ + TensorFrameBuffer(nvidia::gxf::Entity entity, nvidia::gxf::VideoFormat format); + virtual ~TensorFrameBuffer() = default; + byte_t* get() const override { + return (tensor_) ? static_cast(tensor_->pointer()) : nullptr; + } + + protected: + Status validate_format_compliance(nvidia::gxf::VideoFormat fmt) const override; + + private: + nvidia::gxf::Handle tensor_; +}; + +/** + * @class AllocatedVideoBufferFrameBuffer + * @brief Frame buffer implementation for pre-allocated memory buffers. + * + * Used primarily by the RX operator for receiving frames. + */ +class AllocatedVideoBufferFrameBuffer : public VideoFrameBufferBase { + public: + /** + * @brief Constructs an AllocatedViddeoBufferFrameBuffer from pre-allocated memory. + * + * @param data Pointer to the allocated memory + * @param size Size of the allocated memory in bytes + * @param width Frame width + * @param height Frame height + * @param format Video format + * @param storage_type Memory storage type (device or host) + */ + AllocatedVideoBufferFrameBuffer( + void* data, size_t size, uint32_t width, uint32_t height, nvidia::gxf::VideoFormat format, + nvidia::gxf::MemoryStorageType storage_type = nvidia::gxf::MemoryStorageType::kDevice); + virtual ~AllocatedVideoBufferFrameBuffer() = default; + + byte_t* get() const override { return static_cast(data_); } + + /** + * @brief Creates a GXF entity containing this frame's data. + * + * @param context GXF context for entity creation + * @param release_func Function to call when the entity is released + * @return Created GXF entity + */ + nvidia::gxf::Entity wrap_in_entity( + void* context, std::function(void*)> release_func); + + protected: + Status validate_format_compliance(nvidia::gxf::VideoFormat fmt) const override; + + private: + void* data_; +}; + +/** + * @class AllocatedTensorFrameBuffer + * @brief Frame buffer implementation for pre-allocated tensor memory buffers. + * + * Used primarily by the RX operator for receiving frames in tensor format. + */ +class AllocatedTensorFrameBuffer : public VideoFrameBufferBase { + public: + /** + * @brief Constructs an AllocatedTensorFrameBuffer from pre-allocated memory. + * + * @param data Pointer to the allocated memory + * @param size Size of the allocated memory in bytes + * @param width Frame width + * @param height Frame height + * @param channels Number of channels (typically 3 for RGB) + * @param format Video format + * @param storage_type Memory storage type (device or host) + */ + AllocatedTensorFrameBuffer( + void* data, size_t size, uint32_t width, uint32_t height, uint32_t channels, + nvidia::gxf::VideoFormat format, + nvidia::gxf::MemoryStorageType storage_type = nvidia::gxf::MemoryStorageType::kDevice); + virtual ~AllocatedTensorFrameBuffer() = default; + + byte_t* get() const override { return static_cast(data_); } + + /** + * @brief Creates a GXF entity containing this frame's data as a tensor. + * + * @param context GXF context for entity creation + * @param release_func Function to call when the entity is released + * @return Created GXF entity + */ + nvidia::gxf::Entity wrap_in_entity( + void* context, std::function(void*)> release_func); + + protected: + Status validate_format_compliance(nvidia::gxf::VideoFormat fmt) const override; + + private: + void* data_; + uint32_t channels_; +}; + +} // namespace holoscan::ops + +#endif // OPERATORS_ADVANCED_NETWORK_MEDIA_COMMON_FRAME_BUFFER_H_ diff --git a/operators/advanced_network_media/common/rtp_params.h b/operators/advanced_network_media/common/rtp_params.h new file mode 100644 index 0000000000..1309070af6 --- /dev/null +++ b/operators/advanced_network_media/common/rtp_params.h @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OPERATORS_ADVANCED_NETWORK_MEDIA_COMMON_RTP_PARAMS_H_ +#define OPERATORS_ADVANCED_NETWORK_MEDIA_COMMON_RTP_PARAMS_H_ + +#include +#include "rdk/services/services.h" + +// Structure to hold parsed RTP parameters +struct RtpParams { + uint32_t sequence_number; + uint32_t timestamp; + bool m_bit; // Marker bit - indicates end of frame + bool f_bit; // Field bit + uint16_t payload_size; // Payload size from SRD field + + RtpParams() : sequence_number(0), timestamp(0), m_bit(false), f_bit(false), payload_size(0) {} + + /** + * @brief Parse RTP header and populate this struct with extracted parameters + * @param rtp_hdr Pointer to RTP header data + * @return True if parsing successful, false if invalid RTP header + */ + bool parse(const uint8_t* rtp_hdr) { + // Validate RTP version (must be version 2) + if ((rtp_hdr[0] & 0xC0) != 0x80) { + return false; + } + + // Extract CSRC count and calculate offset + uint8_t cc = 0x0F & rtp_hdr[0]; + uint8_t offset = cc * RTP_HEADER_CSRC_GRANULARITY_BYTES; + + // Extract sequence number (16-bit + 16-bit extended) + // Cast to uint32_t before shifting to avoid undefined behavior from integer promotion + sequence_number = static_cast(rtp_hdr[3]) | (static_cast(rtp_hdr[2]) << 8); + sequence_number |= (static_cast(rtp_hdr[offset + 12]) << 24) | + (static_cast(rtp_hdr[offset + 13]) << 16); + + // Extract field bit + f_bit = !!(rtp_hdr[offset + 16] & 0x80); + + // Extract timestamp (32-bit, network byte order = big-endian) + timestamp = (static_cast(rtp_hdr[4]) << 24) | + (static_cast(rtp_hdr[5]) << 16) | + (static_cast(rtp_hdr[6]) << 8) | + static_cast(rtp_hdr[7]); + + // Extract marker bit + m_bit = !!(rtp_hdr[1] & 0x80); + + // Extract payload size from SRD Length field (2 bytes, network byte order) + // Cast to uint16_t before shifting to avoid undefined behavior from integer promotion + payload_size = (static_cast(rtp_hdr[offset + 14]) << 8) | + static_cast(rtp_hdr[offset + 15]); + + return true; + } +}; + +/** + * @brief Parse RTP header and extract all parameters + * @param rtp_hdr Pointer to RTP header data + * @param params Reference to RtpParams struct to populate + * @return True if parsing successful, false if invalid RTP header + */ +inline bool parse_rtp_header(const uint8_t* rtp_hdr, RtpParams& params) { + return params.parse(rtp_hdr); +} +#endif // OPERATORS_ADVANCED_NETWORK_MEDIA_COMMON_RTP_PARAMS_H_ diff --git a/operators/advanced_network_media/common/video_parameters.cpp b/operators/advanced_network_media/common/video_parameters.cpp new file mode 100644 index 0000000000..427f3e72f2 --- /dev/null +++ b/operators/advanced_network_media/common/video_parameters.cpp @@ -0,0 +1,146 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "video_parameters.h" +#include "adv_network_media_logging.h" + +namespace holoscan::ops { + +VideoFormatSampling get_video_sampling_format(const std::string& format) { + // Convert input format to lowercase for case-insensitive comparison + std::string format_lower = format; + std::transform( + format_lower.begin(), format_lower.end(), format_lower.begin(), [](unsigned char c) { + return std::tolower(c); + }); + + // Check against lowercase versions of format strings + if (format_lower == "rgb888" || format_lower == "rgb") return VideoFormatSampling::RGB; + + // YCbCr 4:2:2 / YUV 4:2:2 formats + if (format_lower == "ycbcr-4:2:2" || format_lower == "yuv422" || format_lower == "yuv-422" || + format_lower == "yuv-4:2:2" || format_lower == "ycbcr422") + return VideoFormatSampling::YCbCr_4_2_2; + + // YCbCr 4:2:0 / YUV 4:2:0 formats + if (format_lower == "ycbcr-4:2:0" || format_lower == "yuv420" || format_lower == "yuv-420" || + format_lower == "yuv-4:2:0" || format_lower == "ycbcr420") + return VideoFormatSampling::YCbCr_4_2_0; + + // YCbCr 4:4:4 / YUV 4:4:4 formats + if (format_lower == "ycbcr-4:4:4" || format_lower == "yuv444" || format_lower == "yuv-444" || + format_lower == "yuv-4:4:4" || format_lower == "ycbcr444") + return VideoFormatSampling::YCbCr_4_4_4; + + // Return CUSTOM for any unsupported format + ANM_CONFIG_LOG("Unsupported video sampling format: {}. Using CUSTOM format.", format); + return VideoFormatSampling::CUSTOM; +} + +VideoColorBitDepth get_color_bit_depth(int bit_depth) { + switch (bit_depth) { + case 8: + return VideoColorBitDepth::_8; + case 10: + return VideoColorBitDepth::_10; + case 12: + return VideoColorBitDepth::_12; + default: + throw std::invalid_argument("Unsupported bit depth: " + std::to_string(bit_depth)); + } +} + +nvidia::gxf::VideoFormat get_expected_gxf_video_format(VideoFormatSampling sampling, + VideoColorBitDepth depth) { + if (sampling == VideoFormatSampling::RGB && depth == VideoColorBitDepth::_8) { + return nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_RGB; + } else if (sampling == VideoFormatSampling::YCbCr_4_2_0 && depth == VideoColorBitDepth::_8) { + return nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_NV12_709; + } else if (sampling == VideoFormatSampling::CUSTOM) { + return nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_CUSTOM; + } else { + // Return CUSTOM for any unsupported format + return nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_CUSTOM; + } +} + +size_t calculate_frame_size(uint32_t width, uint32_t height, VideoFormatSampling sampling_format, + VideoColorBitDepth bit_depth) { + using BytesPerPixelRatio = std::pair; + using ColorDepthPixelRatioMap = + std::unordered_map>; + + static const ColorDepthPixelRatioMap COLOR_DEPTH_TO_PIXEL_RATIO = { + {VideoFormatSampling::RGB, + {{VideoColorBitDepth::_8, {3, 1}}, + {VideoColorBitDepth::_10, {15, 4}}, + {VideoColorBitDepth::_12, {9, 2}}}}, + {VideoFormatSampling::YCbCr_4_4_4, + {{VideoColorBitDepth::_8, {3, 1}}, + {VideoColorBitDepth::_10, {15, 4}}, + {VideoColorBitDepth::_12, {9, 2}}}}, + {VideoFormatSampling::YCbCr_4_2_2, + {{VideoColorBitDepth::_8, {4, 2}}, + {VideoColorBitDepth::_10, {5, 2}}, + {VideoColorBitDepth::_12, {6, 2}}}}, + {VideoFormatSampling::YCbCr_4_2_0, + {{VideoColorBitDepth::_8, {6, 4}}, + {VideoColorBitDepth::_10, {15, 8}}, + {VideoColorBitDepth::_12, {9, 4}}}}}; + + auto format_it = COLOR_DEPTH_TO_PIXEL_RATIO.find(sampling_format); + if (format_it == COLOR_DEPTH_TO_PIXEL_RATIO.end()) { + throw std::invalid_argument("Unsupported sampling format"); + } + + auto depth_it = format_it->second.find(bit_depth); + if (depth_it == format_it->second.end()) { throw std::invalid_argument("Unsupported bit depth"); } + + // Use 64-bit integer arithmetic to avoid float truncation and overflow on large dimensions + uint64_t total_units = static_cast(width) * static_cast(height) * + static_cast(depth_it->second.first); + uint64_t size_bytes = (total_units + depth_it->second.second - 1) / depth_it->second.second; + return static_cast(size_bytes); +} + +uint32_t get_channel_count_for_format(nvidia::gxf::VideoFormat format) { + switch (format) { + // RGB formats + case nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_RGB: + return 3; + case nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_RGBA: + return 4; + // NV12 formats (semi-planar with interleaved UV) - all use 2 channels + case nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_NV12: + case nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_NV12_ER: + case nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_NV12_709: + case nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_NV12_709_ER: + return 2; + // YUV420 formats (multi-planar) - all use 3 planes (Y, U, V) + case nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_YUV420: + case nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_YUV420_ER: + case nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_YUV420_709: + case nvidia::gxf::VideoFormat::GXF_VIDEO_FORMAT_YUV420_709_ER: + return 3; + default: + ANM_LOG_WARN("Unknown format {}, assuming 3 channels", static_cast(format)); + return 3; + } +} + +} // namespace holoscan::ops diff --git a/operators/advanced_network_media/common/video_parameters.h b/operators/advanced_network_media/common/video_parameters.h new file mode 100644 index 0000000000..74ac2519b5 --- /dev/null +++ b/operators/advanced_network_media/common/video_parameters.h @@ -0,0 +1,102 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OPERATORS_ADVANCED_NETWORK_MEDIA_COMMON_VIDEO_PARAMETERS_H_ +#define OPERATORS_ADVANCED_NETWORK_MEDIA_COMMON_VIDEO_PARAMETERS_H_ + +#include +#include +#include "gxf/multimedia/video.hpp" +#include +#include "advanced_network/common.h" + +namespace holoscan::ops { + +/** + * @enum VideoFormatSampling + * @brief Enumeration for video sampling formats. + */ +enum class VideoFormatSampling { + RGB, + YCbCr_4_4_4, + YCbCr_4_2_2, + YCbCr_4_2_0, + CUSTOM // Default for unsupported formats +}; + +/** + * @enum VideoColorBitDepth + * @brief Enumeration for video color bit depths. + */ +enum class VideoColorBitDepth { _8, _10, _12 }; + +/** + * @brief Converts a string format name to a VideoFormatSampling enum value. + * + * Supported formats include RGB888, YCbCr-4:2:2, YCbCr-4:2:0, YCbCr-4:4:4, + * and simplified notations like yuv422, yuv420, yuv444. The comparison is + * case-insensitive. + * + * @param format String representation of the video format. + * @return The corresponding VideoFormatSampling enum value. + * Returns VideoFormatSampling::CUSTOM for unsupported formats. + */ +VideoFormatSampling get_video_sampling_format(const std::string& format); + +/** + * @brief Converts a bit depth integer to a VideoColorBitDepth enum value. + * + * @param bit_depth Integer representation of the bit depth. + * @return The corresponding VideoColorBitDepth enum value. + * @throws std::invalid_argument If the bit depth is not supported. + */ +VideoColorBitDepth get_color_bit_depth(int bit_depth); + +/** + * @brief Maps internal video format representation to GXF video format. + * + * @param sampling The video sampling format. + * @param depth The color bit depth. + * @return The GXF video format corresponding to the given settings. + */ +nvidia::gxf::VideoFormat get_expected_gxf_video_format(VideoFormatSampling sampling, + VideoColorBitDepth depth); + +/** + * @brief Calculates the frame size based on resolution, sampling format, and bit depth. + * + * @param width Frame width in pixels. + * @param height Frame height in pixels. + * @param sampling_format The video sampling format. + * @param bit_depth The color bit depth. + * @return The calculated frame size in bytes. + * @throws std::invalid_argument If the sampling format or bit depth is unsupported. + */ +size_t calculate_frame_size(uint32_t width, uint32_t height, VideoFormatSampling sampling_format, + VideoColorBitDepth bit_depth); + +/** + * @brief Returns the number of channels required for a given video format + * + * @param format The GXF video format + * @return Number of channels required for this format + */ +uint32_t get_channel_count_for_format(nvidia::gxf::VideoFormat format); + +} // namespace holoscan::ops + +#endif // OPERATORS_ADVANCED_NETWORK_MEDIA_COMMON_VIDEO_PARAMETERS_H_ From b31f5b40dedebd94bbaeb0294ee2cb4707c860f1 Mon Sep 17 00:00:00 2001 From: Rony Rado Date: Sun, 29 Jun 2025 20:36:30 +0000 Subject: [PATCH 2/6] feat(adv_networking_media_sender) Add Adv. Networking Media Sender MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add high-performance media streaming application built on Advanced Network Media Tx operator - Utilizes AdvNetworkMediaTxOp for media transmission over Rivermax - Integrates with Advanced Network Manager for optimized network resource management - SMPTE 2110 compliant for professional broadcast applications - Real-time transmission of media files with precise timing control - Support multiple video formats and resolutions - RGB888, YUV420, NV12 and other common video formats - Configurable bit depths (8, 10, 12, 16-bit) - Multiple resolution support up to 4K and beyond - GPU acceleration with GPUDirect for zero-copy operations * Implement dual language support - C++ implementation for maximum performance - Python implementation for ease of use and development * Support professional broadcast features - Frame-accurate timing for live streaming applications - Looping playback for continuous streaming - VideoStreamReplayer integration for file-based sources Pipeline: VideoStreamReplayer → AdvNetworkMediaTxOp → Advanced Network Manager → Network Interface This application demonstrates how to use the Advanced Network Media operators for professional-grade media transmission in broadcast and media production environments requiring ultra-low latency and high throughput performance. Signed-off-by: Rony Rado --- applications/CMakeLists.txt | 3 + .../CMakeLists.txt | 20 + .../adv_networking_media_sender/README.md | 525 ++++++++++++++++++ .../adv_networking_media_sender.yaml | 102 ++++ .../cpp/CMakeLists.txt | 80 +++ .../cpp/adv_networking_media_sender.cpp | 87 +++ .../cpp/metadata.json | 39 ++ .../python/CMakeLists.txt | 30 + .../python/adv_networking_media_sender.py | 222 ++++++++ .../python/metadata.json | 39 ++ 10 files changed, 1147 insertions(+) create mode 100755 applications/adv_networking_media_sender/CMakeLists.txt create mode 100755 applications/adv_networking_media_sender/README.md create mode 100755 applications/adv_networking_media_sender/adv_networking_media_sender.yaml create mode 100755 applications/adv_networking_media_sender/cpp/CMakeLists.txt create mode 100755 applications/adv_networking_media_sender/cpp/adv_networking_media_sender.cpp create mode 100755 applications/adv_networking_media_sender/cpp/metadata.json create mode 100755 applications/adv_networking_media_sender/python/CMakeLists.txt create mode 100755 applications/adv_networking_media_sender/python/adv_networking_media_sender.py create mode 100755 applications/adv_networking_media_sender/python/metadata.json diff --git a/applications/CMakeLists.txt b/applications/CMakeLists.txt index 380abe6f31..5ff6f37c33 100644 --- a/applications/CMakeLists.txt +++ b/applications/CMakeLists.txt @@ -17,6 +17,9 @@ add_holohub_application(adv_networking_bench DEPENDS OPERATORS advanced_network) +add_holohub_application(adv_networking_media_sender DEPENDS + OPERATORS advanced_network advanced_network_media_tx) + add_holohub_application(aja_video_capture DEPENDS OPERATORS aja_source) diff --git a/applications/adv_networking_media_sender/CMakeLists.txt b/applications/adv_networking_media_sender/CMakeLists.txt new file mode 100755 index 0000000000..2e6d8900fa --- /dev/null +++ b/applications/adv_networking_media_sender/CMakeLists.txt @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Add subdirectories +add_subdirectory(cpp) +if(HOLOHUB_BUILD_PYTHON) + add_subdirectory(python) +endif() diff --git a/applications/adv_networking_media_sender/README.md b/applications/adv_networking_media_sender/README.md new file mode 100755 index 0000000000..9d818566ce --- /dev/null +++ b/applications/adv_networking_media_sender/README.md @@ -0,0 +1,525 @@ +# Advanced Networking Media Sender + +The Advanced Networking Media Sender is a high-performance application for transmitting media streams over advanced network infrastructure using NVIDIA's Rivermax SDK. This application demonstrates professional-grade media streaming capabilities with ultra-low latency and high throughput for broadcast and media production environments. + +## Overview + +This application showcases high-performance media transmission over IP networks, utilizing NVIDIA's advanced networking technologies. It reads media files from disk and transmits them as real-time streams using the SMPTE 2110 standard, making it ideal for professional broadcast applications. + +### Key Features + +- **High-Performance Streaming**: Transmit media streams with minimal latency using Rivermax SDK +- **SMPTE 2110 Compliance**: Industry-standard media over IP protocol support +- **File-based Source**: Read and stream media files with precise timing control +- **GPU Acceleration**: Leverage GPUDirect for zero-copy operations +- **Multiple Format Support**: RGB888, YUV420, NV12, and other common video formats +- **Real-time Playback**: Accurate frame rate control for live streaming applications + +### Application Architecture + +The Advanced Networking Media Sender implements a sophisticated frame-level processing architecture with configurable memory management strategies for optimal performance in different use cases. + +#### Complete Application Data Flow + +```mermaid +graph TD + %% Application Source Layer + A["VideoStreamReplayer
(File Reading)"] --> B["GXF Entity Generation
(VideoBuffer/Tensor)"] + + %% Media TX Operator Layer + B --> C["AdvNetworkMediaTxOp"] + C --> D["Frame Validation
& Format Check"] + D --> E{{"Input Entity Type"}} + E -->|VideoBuffer| F["VideoBufferFrameBuffer
(Wrapper)"] + E -->|Tensor| G["TensorFrameBuffer
(Wrapper)"] + + %% MediaFrame Processing + F --> H["MediaFrame Creation
(Reference Only - No Copy)"] + G --> H + H --> I["BurstParams Creation"] + I --> J["MediaFrame Attachment
(via custom_pkt_data)"] + + %% Advanced Network Manager + J --> K["RivermaxMgr
(Advanced Network Manager)"] + K --> L{{"Service Selection
(Configuration Driven)"}} + + %% Service Path Selection + L -->|use_internal_memory_pool: false| M["MediaSenderZeroCopyService
(True Zero-Copy Path)"] + L -->|use_internal_memory_pool: true| N["MediaSenderService
(Single Copy + Pool Path)"] + + %% Zero-Copy Path + M --> O["No Internal Memory Pool"] + O --> P["Direct Frame Reference
(custom_pkt_data → RDK)"] + P --> Q["RDK MediaSenderApp
(Zero-Copy Processing)"] + Q --> R["Frame Ownership Transfer
& Release After Processing"] + + %% Memory Pool Path + N --> S["Pre-allocated MediaFramePool
(MEDIA_FRAME_POOL_SIZE buffers)"] + S --> T["Single Memory Copy
(Application Frame → Pool Buffer)"] + T --> U["RDK MediaSenderApp
(Pool Buffer Processing)"] + U --> V["Pool Buffer Reuse
(Returned to Pool)"] + + %% RDK Processing (Common Path) + Q --> W["RDK Internal Processing
(All Packet Operations)"] + U --> W + W --> X["RTP Packetization
(SMPTE 2110 Standard)"] + X --> Y["Protocol Headers
& Metadata Addition"] + Y --> Z["Precise Timing Control
& Scheduling"] + + %% Network Hardware + Z --> AA["Rivermax Hardware
Acceleration"] + AA --> BB["ConnectX NIC
Hardware Queues"] + BB --> CC["Network Interface
Transmission"] + + %% Styling + classDef appLayer fill:#fff3e0 + classDef operatorLayer fill:#e8f5e8 + classDef managerLayer fill:#f3e5f5 + classDef zeroCopyPath fill:#e8f5e8,stroke:#4caf50,stroke-width:3px + classDef poolPath fill:#fff8e1,stroke:#ff9800,stroke-width:3px + classDef rdkLayer fill:#e3f2fd + classDef networkLayer fill:#e1f5fe + classDef configDecision fill:#f9f9f9,stroke:#333,stroke-width:2px + + class A,B appLayer + class C,D,E,F,G,H,I,J operatorLayer + class K managerLayer + class L configDecision + class M,O,P,Q,R zeroCopyPath + class N,S,T,U,V poolPath + class W,X,Y,Z rdkLayer + class AA,BB,CC networkLayer +``` + +#### Service Selection Decision Flow + +```mermaid +flowchart TD + A["Application Configuration
(use_internal_memory_pool)"] --> B{{"Memory Pool
Configuration"}} + + B -->|false| C["MediaSenderZeroCopyService
(Pipeline Mode)"] + B -->|true| D["MediaSenderService
(Data Generation Mode)"] + + %% Zero-Copy Path Details + C --> E["Zero Memory Copies
Only Frame Reference Transfer"] + E --> F["Maximum Latency Efficiency
Minimal Memory Usage"] + F --> G["Best for: Pipeline Processing
Real-time Applications"] + + %% Memory Pool Path Details + D --> H["Single Memory Copy
Application → Pool Buffer"] + H --> I["Sustained High Throughput
Buffer Pool Reuse"] + I --> J["Best for: File Reading
Batch Processing"] + + %% Common Processing + G --> K["RDK MediaSenderApp
Processing"] + J --> K + K --> L["RTP Packetization
Network Transmission"] + + %% Usage Examples + M["Usage Scenarios"] --> N["Pipeline Mode Applications"] + M --> O["Data Generation Applications"] + + N --> P["• Frame-to-frame operators
• Real-time transformations
• Low-latency streaming
• GPU-accelerated processing"] + O --> Q["• File readers (VideoStreamReplayer)
• Camera/sensor operators
• Synthetic data generators
• Batch processing systems"] + + P -.-> C + Q -.-> D + + %% Styling + classDef configNode fill:#f9f9f9,stroke:#333,stroke-width:2px + classDef zeroCopyPath fill:#e8f5e8,stroke:#4caf50,stroke-width:3px + classDef poolPath fill:#fff8e1,stroke:#ff9800,stroke-width:3px + classDef rdkLayer fill:#e3f2fd + classDef usageLayer fill:#f3e5f5 + + class A,B configNode + class C,E,F,G zeroCopyPath + class D,H,I,J poolPath + class K,L rdkLayer + class M,N,O,P,Q usageLayer +``` + +#### Simplified Application Pipeline + +``` +Video Files → VideoStreamReplayer → AdvNetworkMediaTxOp → RDK Services → Network +``` + +## Requirements + +### Hardware Requirements +- Linux system (x86_64 or aarch64) +- NVIDIA NIC with ConnectX-6 or later chip +- NVIDIA GPU (for GPU acceleration) +- Sufficient network bandwidth for target media streams +- Storage with adequate throughput for media file reading + +### Software Requirements +- NVIDIA Rivermax SDK +- NVIDIA GPU drivers +- MOFED drivers (5.8-1.0.1.1 or later) +- DOCA 2.7 or later (if using DOCA backend) +- System tuning as described in the [High Performance Networking tutorial](../../tutorials/high_performance_networking/README.md) + +## Build Instructions + +### Build Docker Image + +Build the Docker image and application with Rivermax support: + +**C++ version:** +```bash +./holohub build adv_networking_media_sender --build-args="--target rivermax" --configure-args="-D ANO_MGR:STRING=rivermax" --language cpp +``` + +**Python version:** +```bash +./holohub build adv_networking_media_sender --build-args="--target rivermax" --configure-args="-D ANO_MGR:STRING=rivermax" --language python +``` + +### Launch Container + +Launch the Rivermax-enabled container: + +**C++ version:** +```bash +./holohub run-container adv_networking_media_sender --build-args="--target rivermax" --docker-opts="-u root --privileged -v /opt/mellanox/rivermax/rivermax.lic:/opt/mellanox/rivermax/rivermax.lic -v /media/video:/media/video/ -w /workspace/holohub/build/adv_networking_media_sender/applications/adv_networking_media_sender/cpp" +``` + +**Python version:** +```bash +./holohub run-container adv_networking_media_sender --build-args="--target rivermax" --docker-opts="-u root --privileged -v /opt/mellanox/rivermax/rivermax.lic:/opt/mellanox/rivermax/rivermax.lic -v /media/video:/media/video/ -w /workspace/holohub/build/adv_networking_media_sender/applications/adv_networking_media_sender/python" +``` + +## Running the Application + +### Prerequisites + +Before running, ensure your environment is properly configured: + +```bash +# Update PYTHONPATH for Python applications +# Note: Run this command from the container's working directory +# (as set by -w flag in run-container command) +export PYTHONPATH=${PYTHONPATH}:/opt/nvidia/holoscan/python/lib:$PWD/../../../python/lib:$PWD + +# Ensure proper system configuration (run as root if needed) +# See High Performance Networking tutorial for system tuning +``` + +### C++ Application + +```bash +./adv_networking_media_sender adv_networking_media_sender.yaml +``` + +### Python Application + +```bash +python3 ./adv_networking_media_sender.py adv_networking_media_sender.yaml +``` + +## Configuration + +The application uses a YAML configuration file that defines the complete transmission pipeline. The configuration has four main sections: + +1. **Advanced Network Manager Configuration**: Network interfaces, memory regions, and Rivermax TX settings +2. **Media TX Operator Configuration**: Video format, frame dimensions, and interface settings +3. **VideoStreamReplayer Configuration**: Source file path and playback options +4. **Memory Allocator Configuration**: GPU and host memory settings + +> **📁 Example Configuration Files**: +> - `applications/adv_networking_media_sender/adv_networking_media_sender.yaml` - Standard 1080p configuration + +> **For detailed configuration parameter documentation**, see: +> - [Advanced Network Operator Configuration](../../operators/advanced_network/README.md) - Network settings, memory regions, Rivermax TX settings +> - [Advanced Network Media TX Operator Configuration](../../operators/advanced_network_media/README.md) - Service selection, memory pool modes, TX data flow, performance characteristics + +### Quick Reference: Key Parameters That Must Match + +Critical parameters must be consistent across configuration sections to ensure proper operation: + +| Parameter Category | Section 1 | Section 2 | Example Values | Required Match | +|-------------------|-----------|-----------|----------------|----------------| +| **Frame Rate** | `replayer.frame_rate` | `rivermax_tx_settings.frame_rate` | 60 | ✓ Must match exactly | +| **Frame Dimensions** | `advanced_network_media_tx.frame_width/height` | `rivermax_tx_settings.frame_width/height` | 1920x1080 | ✓ Must match exactly | +| **Video Format** | `advanced_network_media_tx.video_format` | `rivermax_tx_settings.video_format` | RGB888 / RGB | ✓ Must be compatible | +| **Bit Depth** | `advanced_network_media_tx.bit_depth` | `rivermax_tx_settings.bit_depth` | 8 | ✓ Must match exactly | +| **Interface** | `advanced_network_media_tx.interface_name` | `advanced_network.interfaces.address` | cc:00.1 | ✓ Must match exactly | +| **Memory Location** | `rivermax_tx_settings.memory_pool_location` | Memory region types (`host`/`device`) | device | ✓ Should be consistent | + +> **⚠️ IMPORTANT: Configuration Parameter Consistency** +> +> Parameters across configuration sections must be consistent and properly matched: +> - **Video Format Matching**: `video_format` parameters must match across Media TX operator and Rivermax TX settings +> - **Frame Dimensions**: `frame_width` and `frame_height` must match between operator and RDK settings +> - **Frame Rate**: VideoStreamReplayer `frame_rate` must match Rivermax TX `frame_rate` for proper timing +> - **Memory Buffer Sizing**: `buf_size` in memory regions depends on video format, resolution, and packet size +> - For RGB888 @ 1920x1080: Typical payload size is ~1440 bytes per packet +> - **Memory Location**: `memory_pool_location` should match memory region types configured (`host` vs `device`) +> - **Interface Matching**: `interface_name` (Media TX) must match the interface address/name in Advanced Network config +> - **Service Mode Selection**: `use_internal_memory_pool` determines MediaSender service behavior (see operator documentation) +> +> Mismatched parameters will result in runtime errors or degraded performance. + +### Configuration File Structure + +The application configuration consists of four main sections: + +#### 1. Advanced Network Manager Configuration + +Configures network interfaces, memory regions, and Rivermax TX settings. See [Advanced Network Operator documentation](../../operators/advanced_network/README.md) for detailed parameter descriptions. + +```yaml +advanced_network: + cfg: + version: 1 + manager: "rivermax" + master_core: 6 # Master CPU core + debug: 1 + log_level: "debug" + + memory_regions: + - name: "Data_TX_CPU" + kind: "huge" + affinity: 0 + num_bufs: 43200 + buf_size: 20 + - name: "Data_TX_GPU" + kind: "device" + affinity: 0 + num_bufs: 43200 + buf_size: 1440 + + interfaces: + - name: "tx_port" + address: cc:00.1 + tx: + queues: + - name: "tx_q_1" + id: 0 + cpu_core: "12" + batch_size: 4320 + output_port: "bench_tx_out_1" + memory_regions: + - "Data_TX_CPU" + - "Data_TX_GPU" + rivermax_tx_settings: + settings_type: "media_sender" + memory_registration: true + memory_allocation: true + memory_pool_location: "device" + #allocator_type: "huge_page_2mb" + verbose: true + sleep_between_operations: false + local_ip_address: 2.1.0.12 + destination_ip_address: 224.1.1.2 + destination_port: 50001 + stats_report_interval_ms: 1000 + send_packet_ext_info: true + num_of_packets_in_chunk: 144 + video_format: RGB + bit_depth: 8 + frame_width: 1920 + frame_height: 1080 + frame_rate: 60 + dummy_sender: false +``` + +**Key Rivermax TX Settings**: +- `settings_type: "media_sender"` - Uses MediaSender RDK service for file-based streaming +- `memory_pool_location: "device"` - Allocates memory pool in GPU memory for optimal performance +- `memory_allocation: true` - Enables internal memory pool allocation (recommended for VideoStreamReplayer) +- `local_ip_address` - Source IP address for transmission +- `destination_ip_address` - Target IP address (multicast supported: 224.0.0.0 - 239.255.255.255) +- `destination_port` - Target UDP port for stream delivery +- `frame_rate` - Network transmission frame rate (must match `replayer.frame_rate`) +- `video_format` - Video pixel format (RGB, YUV, etc.) +- `bit_depth` - Color bit depth (8, 10, 12, 16) +- `num_of_packets_in_chunk` - Number of packets per network transmission chunk + +#### 2. Media TX Operator Configuration + +Configures video format, frame dimensions, and interface settings. See [Advanced Network Media TX Operator documentation](../../operators/advanced_network_media/README.md) for detailed parameter descriptions. + +```yaml +advanced_network_media_tx: + interface_name: cc:00.1 + video_format: RGB888 + frame_width: 1920 + frame_height: 1080 + bit_depth: 8 +``` + +#### 3. VideoStreamReplayer Configuration + +Configures source file path and playback options. Files must be in GXF entity format (see [Media File Preparation](#media-file-preparation)): + +```yaml +replayer: + directory: "/media/video" # Path to directory containing GXF entity files + basename: "bunny" # Base name matching converted files (bunny.gxf_entities, bunny.gxf_index) + frame_rate: 60 # Must match rivermax_tx_settings.frame_rate + repeat: true # Loop playback (true/false) + realtime: true # Real-time playback timing (true/false) + count: 0 # Number of frames to transmit (0 = unlimited) +``` + +**Key VideoStreamReplayer Settings**: +- `directory` - Path to media files directory containing `.gxf_entities` and `.gxf_index` files +- `basename` - Base name for media files (must match converted GXF entity file names) +- `frame_rate` - Target frame rate for transmission (must match `rivermax_tx_settings.frame_rate`) +- `repeat` - Enable looping playback for continuous streaming +- `realtime` - Enable real-time playback timing to maintain accurate frame rates +- `count` - Number of frames to transmit (0 = unlimited, useful for testing with specific frame counts) + +#### 4. Memory Allocator Configuration + +Configures GPU and host memory allocation: + +```yaml +rmm_allocator: + device_memory_initial_size: "1024 MB" + device_memory_max_size: "1024 MB" + host_memory_initial_size: "1024 MB" + host_memory_max_size: "1024 MB" + dev_id: 0 +``` + +## Media File Preparation + +### Supported Formats + +The VideoStreamReplayerOp expects video data encoded as GXF entities, not standard video files. The application requires: +- **GXF Entity Format**: Video streams encoded as `.gxf_entities` and `.gxf_index` files +- **Directory structure**: GXF files should be organized in a directory +- **Naming convention**: `.gxf_entities` and `.gxf_index` + +### Converting Media Files + +To convert standard video files to the required GXF entity format, use the provided conversion script: + +```bash +# Convert video file to GXF entities +# Script is available in /opt/nvidia/holoscan/bin or on GitHub +convert_video_to_gxf_entities.py --input input_video.mp4 --output_dir /media/video --basename bunny + +# This will create: +# - /media/video/bunny.gxf_entities +# - /media/video/bunny.gxf_index +``` + +### Video Conversion Parameters + +The conversion script supports various options: + +```bash +# Basic conversion with custom resolution +convert_video_to_gxf_entities.py \ + --input input_video.mp4 \ + --output_dir /media/video \ + --basename bunny \ + --width 1920 \ + --height 1080 \ + --framerate 60 + +# For specific pixel formats +convert_video_to_gxf_entities.py \ + --input input_video.mp4 \ + --output_dir /media/video \ + --basename bunny \ + --pixel_format rgb24 +``` + +## Troubleshooting + +### Common Issues + +1. **File Not Found**: Verify media file paths and naming conventions +2. **Network Errors**: Check IP addresses, ports, and network connectivity +3. **Performance Issues**: Review system tuning and resource allocation +4. **Memory Errors**: Adjust buffer sizes and memory allocations + +### Debug Options + +Enable debug logging by setting `log_level: "debug"` in the advanced_network configuration section. + +### Network Testing + +Test network connectivity before running the application: + +```bash +# Test multicast connectivity +ping 224.1.1.2 + +# Verify network interface configuration +ip addr show +``` + +## Performance Optimization + +### Configuration Optimization + +For optimal performance, configure the following based on your use case: + +#### For VideoStreamReplayer Applications (File-based Streaming) + +This application uses VideoStreamReplayer which generates data from files, making it a **data generation** use case. Recommended configuration: + +1. **Service Mode**: Set `use_internal_memory_pool: true` for MediaSenderService (memory pool mode) +2. **Memory Location**: Use `memory_pool_location: "device"` for GPU memory optimization +3. **Memory Allocation**: Enable `memory_allocation: true` for internal pool allocation +4. **Timing Control**: Set `sleep_between_operations: false` for maximum throughput +5. **Frame Rate Matching**: Ensure VideoStreamReplayer `frame_rate` matches Rivermax TX `frame_rate` +6. **Memory Buffer Sizing**: Size buffers appropriately for your frame rate and resolution + +> **For detailed service selection, TX data flow, and optimization strategies**, see: +> - [Advanced Network Media TX Operator Documentation](../../operators/advanced_network_media/README.md) - Service selection, memory pool vs zero-copy modes, TX architecture, performance characteristics +> - [Advanced Network Operator Documentation](../../operators/advanced_network/README.md) - Memory region configuration, queue settings + +### System-Level Tuning + +Ensure your system is properly configured for high-performance networking: + +- **CPU Isolation**: Isolate CPU cores for network processing +- **Memory Configuration**: Configure hugepages and memory allocation +- **GPU Configuration**: Ensure sufficient GPU memory for frame buffering +- **Network Tuning**: Configure interrupt mitigation and CPU affinity + +> **For detailed system tuning guidelines**, see the [High Performance Networking Tutorial](../../tutorials/high_performance_networking/README.md) + +### Performance Monitoring + +Monitor these key metrics for optimal performance: +- **Frame Transmission Rate**: Consistent frame rate without drops +- **Memory Pool Utilization**: Pool buffers are being reused effectively +- **GPU Memory Usage**: Sufficient GPU memory for sustained operation +- **Network Statistics**: Verify timing accuracy and packet delivery + +## Example Use Cases + +### Live Event Streaming +- Stream pre-recorded content as live feeds using VideoStreamReplayer +- Support for multiple concurrent streams with memory pool optimization +- Frame-accurate timing for broadcast applications + +### Content Distribution +- Distribute media content across network infrastructure from file sources +- Support for multicast delivery to multiple receivers +- High-throughput content delivery networks with sustained file-to-network streaming + +### Testing and Development +- Generate test streams for receiver development using loop playback +- Validate network infrastructure performance with realistic file-based sources +- Prototype media streaming applications with known video content + +## Related Documentation + +### Operator Documentation +For detailed implementation information and advanced configuration: +- **[Advanced Network Media TX Operator](../../operators/advanced_network_media/README.md)**: Comprehensive operator documentation and configuration options +- **[Advanced Network Operators](../../operators/advanced_network/README.md)**: Base networking infrastructure and setup + +### Additional Resources +- **[High Performance Networking Tutorial](../../tutorials/high_performance_networking/README.md)**: System tuning and optimization guide +- **[Advanced Networking Media Player](../adv_networking_media_player/README.md)**: Companion application for media reception diff --git a/applications/adv_networking_media_sender/adv_networking_media_sender.yaml b/applications/adv_networking_media_sender/adv_networking_media_sender.yaml new file mode 100755 index 0000000000..82c7638aaa --- /dev/null +++ b/applications/adv_networking_media_sender/adv_networking_media_sender.yaml @@ -0,0 +1,102 @@ +%YAML 1.2 +# SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +scheduler: + check_recession_period_ms: 0 + worker_thread_number: 5 + stop_on_deadlock: true + stop_on_deadlock_timeout: 500 + +dual_window: false + +replayer: + directory: "/media/video" + basename: "bunny" + frame_rate: 60 # as specified in timestamps + repeat: true # default: false + realtime: true # default: true + count: 0 # default: 0 (no frame count restriction) + +# Initial size below is set to 8 MB which is sufficient for +# a 1920 * 1080 RGBA image (uint8_t). +rmm_allocator: + device_memory_initial_size: "1024 MB" + device_memory_max_size: "1024 MB" + host_memory_initial_size: "1024 MB" + host_memory_max_size: "1024 MB" + dev_id: 0 + +advanced_network: + cfg: + version: 1 + manager: "rivermax" + master_core: 6 # Master CPU core + debug: 1 + log_level: "debug" + + memory_regions: + - name: "Data_TX_CPU" + kind: "huge" + affinity: 0 + num_bufs: 43200 + buf_size: 20 + - name: "Data_TX_GPU" + kind: "device" + affinity: 0 + num_bufs: 43200 + buf_size: 1440 + + interfaces: + - name: "tx_port" + address: cc:00.1 + tx: + queues: + - name: "tx_q_1" + id: 0 + cpu_core: "12" + batch_size: 4320 + output_port: "bench_tx_out_1" + memory_regions: + - "Data_TX_CPU" + - "Data_TX_GPU" + rivermax_tx_settings: + settings_type: "media_sender" + memory_registration: true + memory_allocation: true + memory_pool_location: "device" + #allocator_type: "huge_page_2mb" + verbose: true + sleep_between_operations: false + local_ip_address: 2.1.0.12 + destination_ip_address: 224.1.1.2 + destination_port: 50001 + stats_report_interval_ms: 1000 + send_packet_ext_info: true + num_of_packets_in_chunk: 144 + video_format: RGB + bit_depth: 8 + frame_width: 1920 + frame_height: 1080 + frame_rate: 60 + dummy_sender: false + +advanced_network_media_tx: + interface_name: cc:00.1 + video_format: RGB888 + frame_width: 1920 + frame_height: 1080 + bit_depth: 8 + diff --git a/applications/adv_networking_media_sender/cpp/CMakeLists.txt b/applications/adv_networking_media_sender/cpp/CMakeLists.txt new file mode 100755 index 0000000000..2c2d6a48cc --- /dev/null +++ b/applications/adv_networking_media_sender/cpp/CMakeLists.txt @@ -0,0 +1,80 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required(VERSION 3.20) +project(adv_networking_media_sender CXX) + +# Dependencies +find_package(holoscan 2.6 REQUIRED CONFIG + PATHS "/opt/nvidia/holoscan" "/workspace/holoscan-sdk/install") + +# Global variables +set(CMAKE_CUDA_ARCHITECTURES "70;80;90") + +# Create the executable +add_executable(${PROJECT_NAME} + adv_networking_media_sender.cpp +) + +target_link_libraries(${PROJECT_NAME} + PRIVATE + holoscan::core + holoscan::advanced_network + holoscan::ops::video_stream_replayer + holoscan::ops::advanced_network_media_tx +) + +# Copy config file +add_custom_target(adv_networking_media_sender_yaml + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/../adv_networking_media_sender.yaml" ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/../adv_networking_media_sender.yaml" +) +add_dependencies(${PROJECT_NAME} adv_networking_media_sender_yaml) + + +# Installation +install(TARGETS ${PROJECT_NAME} + DESTINATION examples/${PROJECT_NAME} + COMPONENT ${PROJECT_NAME}-cpp) + +install( + FILES + ../adv_networking_media_sender.yaml + DESTINATION examples/${PROJECT_NAME} + COMPONENT ${PROJECT_NAME}-configs + PERMISSIONS OWNER_READ OWNER_WRITE + GROUP_READ + WORLD_READ +) + +install( + FILES CMakeLists.txt.install + RENAME CMakeLists.txt + DESTINATION examples/${PROJECT_NAME} + COMPONENT ${PROJECT_NAME}-cppsrc + PERMISSIONS OWNER_READ OWNER_WRITE + GROUP_READ + WORLD_READ +) + +install( + FILES + adv_networking_media_sender.cpp + DESTINATION examples/${PROJECT_NAME} + COMPONENT ${PROJECT_NAME}-cppsrc + PERMISSIONS OWNER_READ OWNER_WRITE + GROUP_READ + WORLD_READ +) diff --git a/applications/adv_networking_media_sender/cpp/adv_networking_media_sender.cpp b/applications/adv_networking_media_sender/cpp/adv_networking_media_sender.cpp new file mode 100755 index 0000000000..f3f0508152 --- /dev/null +++ b/applications/adv_networking_media_sender/cpp/adv_networking_media_sender.cpp @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include +#include +#include + +#include "holoscan/holoscan.hpp" +#include +#include "advanced_network/common.h" +#include "adv_network_media_tx.h" + +namespace ano = holoscan::advanced_network; +using holoscan::advanced_network::NetworkConfig; +using holoscan::advanced_network::Status; + +class App : public holoscan::Application { + public: + void compose() override { + using namespace holoscan; + + auto adv_net_config = from_config("advanced_network").as(); + if (ano::adv_net_init(adv_net_config) != Status::SUCCESS) { + HOLOSCAN_LOG_ERROR("Failed to configure the Advanced Network manager"); + exit(1); + } + HOLOSCAN_LOG_INFO("Configured the Advanced Network manager"); + + const auto [rx_en, tx_en] = ano::get_rx_tx_configs_enabled(config()); + const auto mgr_type = ano::get_manager_type(config()); + + HOLOSCAN_LOG_INFO("Using Advanced Network manager {}", + ano::manager_type_to_string(mgr_type)); + + if (!tx_en) { + HOLOSCAN_LOG_ERROR("Tx is not enabled. Please enable Tx in the config file."); + exit(1); + } + + auto adv_net_media_tx = make_operator( + "advanced_network_media_tx", from_config("advanced_network_media_tx")); + + ArgList args; + args.add(Arg("allocator", + make_resource("rmm_allocator", from_config("rmm_allocator")))); + + auto replayer = + make_operator("replayer", from_config("replayer"), args); + add_flow(replayer, adv_net_media_tx, {{"output", "input"}}); + } +}; + +int main(int argc, char** argv) { + using namespace holoscan; + auto app = make_application(); + + // Get the configuration + if (argc < 2) { + HOLOSCAN_LOG_ERROR("Usage: {} config_file", argv[0]); + return -1; + } + + std::filesystem::path config_path(argv[1]); + if (!config_path.is_absolute()) { + config_path = std::filesystem::canonical(argv[0]).parent_path() / config_path; + } + app->config(config_path); + app->scheduler(app->make_scheduler("multithread-scheduler", + app->from_config("scheduler"))); + app->run(); + + ano::shutdown(); + return 0; +} diff --git a/applications/adv_networking_media_sender/cpp/metadata.json b/applications/adv_networking_media_sender/cpp/metadata.json new file mode 100755 index 0000000000..20334fd412 --- /dev/null +++ b/applications/adv_networking_media_sender/cpp/metadata.json @@ -0,0 +1,39 @@ +{ + "application": { + "name": "Advanced Networking Media Sender", + "authors": [ + { + "name": "Rony Rado", + "affiliation": "NVIDIA" + } + ], + "language": "C++", + "version": "1.0", + "changelog": { + "1.0": "Initial Release" + }, + "platforms": ["x86_64", "aarch64"], + "tags": ["Networking and Distributed Computing", "UDP", "Ethernet", "IP", "GPUDirect", "Rivermax", "Media", "Media Sender"], + "dockerfile": "operators/advanced_network/Dockerfile", + "holoscan_sdk": { + "minimum_required_version": "2.6.0", + "tested_versions": [ + "2.6.0" + ] + }, + "ranking": 3, + "requirements": { + "operators": [{ + "name": "advanced_network_media_tx", + "version": "1.0" + }, { + "name": "advanced_network", + "version": "1.4" + }] + }, + "run": { + "command": "/adv_networking_media_sender adv_networking_media_sender.yaml", + "workdir": "holohub_bin" + } + } +} diff --git a/applications/adv_networking_media_sender/python/CMakeLists.txt b/applications/adv_networking_media_sender/python/CMakeLists.txt new file mode 100755 index 0000000000..c2edcd14b9 --- /dev/null +++ b/applications/adv_networking_media_sender/python/CMakeLists.txt @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copy adv_networking_media_sender application file +add_custom_target(python_adv_networking_media_sender ALL + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/adv_networking_media_sender.py" ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS "adv_networking_media_sender.py" + BYPRODUCTS "adv_networking_media_sender.py" +) + +# Copy config file +add_custom_target(python_adv_networking_media_sender_yaml + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/../adv_networking_media_sender.yaml" ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS "../adv_networking_media_sender.yaml" + BYPRODUCTS "adv_networking_media_sender.yaml" +) + +add_dependencies(python_adv_networking_media_sender python_adv_networking_media_sender_yaml) diff --git a/applications/adv_networking_media_sender/python/adv_networking_media_sender.py b/applications/adv_networking_media_sender/python/adv_networking_media_sender.py new file mode 100755 index 0000000000..6e1ed238c6 --- /dev/null +++ b/applications/adv_networking_media_sender/python/adv_networking_media_sender.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import sys +from pathlib import Path + +from holoscan.core import Application +from holoscan.operators import VideoStreamReplayerOp +from holoscan.schedulers import MultiThreadScheduler + +from holohub.advanced_network_common import _advanced_network_common as adv_network_common +from holohub.advanced_network_media_tx import _advanced_network_media_tx as adv_network_media_tx + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def check_rx_tx_enabled(app, require_rx=True, require_tx=False): + """ + Check if RX and TX are enabled in the advanced network configuration. + + Args: + app: The Holoscan Application instance + require_rx: Whether RX must be enabled (default: True) + require_tx: Whether TX must be enabled (default: False) + + Returns: + tuple: (rx_enabled, tx_enabled) + + Raises: + SystemExit: If required functionality is not enabled + """ + try: + # Manual parsing of the advanced_network config + adv_net_config_dict = app.kwargs("advanced_network") + + rx_enabled = False + tx_enabled = False + + # Check if there are interfaces with RX/TX configurations + if "cfg" in adv_net_config_dict and "interfaces" in adv_net_config_dict["cfg"]: + for interface in adv_net_config_dict["cfg"]["interfaces"]: + if "rx" in interface: + rx_enabled = True + if "tx" in interface: + tx_enabled = True + + logger.info(f"RX enabled: {rx_enabled}, TX enabled: {tx_enabled}") + + if require_rx and not rx_enabled: + logger.error("RX is not enabled. Please enable RX in the config file.") + sys.exit(1) + + if require_tx and not tx_enabled: + logger.error("TX is not enabled. Please enable TX in the config file.") + sys.exit(1) + + return rx_enabled, tx_enabled + + except Exception as e: + logger.warning(f"Could not check RX/TX status from advanced_network config: {e}") + # Fallback: check if we have the required operator configs + try: + if require_rx: + app.from_config("advanced_network_media_rx") + logger.info("RX is enabled (found advanced_network_media_rx config)") + if require_tx: + app.from_config("advanced_network_media_tx") + logger.info("TX is enabled (found advanced_network_media_tx config)") + return require_rx, require_tx + except Exception as e2: + if require_rx: + logger.error("RX is not enabled. Please enable RX in the config file.") + logger.error(f"Could not find advanced_network_media_rx configuration: {e2}") + sys.exit(1) + if require_tx: + logger.error("TX is not enabled. Please enable TX in the config file.") + logger.error(f"Could not find advanced_network_media_tx configuration: {e2}") + sys.exit(1) + return False, False + + +class App(Application): + def compose(self): + # Initialize advanced network + try: + adv_net_config = self.from_config("advanced_network") + if adv_network_common.adv_net_init(adv_net_config) != adv_network_common.Status.SUCCESS: + logger.error("Failed to configure the Advanced Network manager") + sys.exit(1) + logger.info("Configured the Advanced Network manager") + except Exception as e: + logger.error(f"Failed to get advanced network config or initialize: {e}") + sys.exit(1) + + # Get manager type + try: + mgr_type = adv_network_common.get_manager_type() + logger.info( + f"Using Advanced Network manager {adv_network_common.manager_type_to_string(mgr_type)}" + ) + except Exception as e: + logger.warning(f"Could not get manager type: {e}") + + # Check TX enabled status (require TX for media sender) + check_rx_tx_enabled(self, require_rx=False, require_tx=True) + logger.info("TX is enabled, proceeding with application setup") + + # Create allocator - use RMMAllocator for better performance (matches C++ version) + # the RMMAllocator supported since v2.6 is much faster than the default UnboundedAllocator + try: + from holoscan.resources import RMMAllocator + + allocator = RMMAllocator(self, name="rmm_allocator", **self.kwargs("rmm_allocator")) + logger.info("Using RMMAllocator for better performance") + except Exception as e: + logger.warning(f"Could not create RMMAllocator: {e}") + from holoscan.resources import UnboundedAllocator + + allocator = UnboundedAllocator(self, name="allocator") + logger.info("Using UnboundedAllocator (RMMAllocator not available)") + + # Create video stream replayer + try: + replayer = VideoStreamReplayerOp( + fragment=self, name="replayer", allocator=allocator, **self.kwargs("replayer") + ) + except Exception as e: + logger.error(f"Failed to create VideoStreamReplayerOp: {e}") + sys.exit(1) + + # Create advanced network media TX operator + try: + adv_net_media_tx = adv_network_media_tx.AdvNetworkMediaTxOp( + fragment=self, + name="advanced_network_media_tx", + **self.kwargs("advanced_network_media_tx"), + ) + except Exception as e: + logger.error(f"Failed to create AdvNetworkMediaTxOp: {e}") + sys.exit(1) + + # Set up the pipeline: replayer -> TX operator + try: + self.add_flow(replayer, adv_net_media_tx, {("output", "input")}) + except Exception as e: + logger.error(f"Failed to set up pipeline: {e}") + sys.exit(1) + + # Set up scheduler + try: + scheduler = MultiThreadScheduler( + fragment=self, name="multithread-scheduler", **self.kwargs("scheduler") + ) + self.scheduler(scheduler) + except Exception as e: + logger.error(f"Failed to set up scheduler: {e}") + sys.exit(1) + + logger.info("Application composition completed successfully") + + +def main(): + if len(sys.argv) < 2: + logger.error(f"Usage: {sys.argv[0]} config_file") + sys.exit(1) + + config_path = Path(sys.argv[1]) + + # Convert to absolute path if relative (matching C++ behavior) + if not config_path.is_absolute(): + # Get the directory of the script and make path relative to it + script_dir = Path(sys.argv[0]).parent.resolve() + config_path = script_dir / config_path + + if not config_path.exists(): + logger.error(f"Config file not found: {config_path}") + sys.exit(1) + + logger.info(f"Using config file: {config_path}") + + try: + app = App() + app.config(str(config_path)) + + logger.info("Starting application...") + app.run() + + logger.info("Application finished") + + except Exception as e: + logger.error(f"Application failed: {e}") + import traceback + + traceback.print_exc() + sys.exit(1) + finally: + # Shutdown advanced network (matching C++ behavior) + try: + adv_network_common.shutdown() + logger.info("Advanced Network shutdown completed") + except Exception as e: + logger.warning(f"Error during advanced network shutdown: {e}") + + +if __name__ == "__main__": + main() diff --git a/applications/adv_networking_media_sender/python/metadata.json b/applications/adv_networking_media_sender/python/metadata.json new file mode 100755 index 0000000000..83657d9a71 --- /dev/null +++ b/applications/adv_networking_media_sender/python/metadata.json @@ -0,0 +1,39 @@ +{ + "application": { + "name": "Advanced Networking Media Sender", + "authors": [ + { + "name": "Rony Rado", + "affiliation": "NVIDIA" + } + ], + "language": "Python", + "version": "1.0", + "changelog": { + "1.0": "Initial Release" + }, + "platforms": ["x86_64", "aarch64"], + "tags": ["Networking and Distributed Computing", "UDP", "Ethernet", "IP", "TCP", "Rivermax"], + "dockerfile": "operators/advanced_network/Dockerfile", + "holoscan_sdk": { + "minimum_required_version": "2.6.0", + "tested_versions": [ + "2.6.0" + ] + }, + "ranking": 3, + "requirements": { + "operators": [{ + "name": "advanced_network_media_tx", + "version": "1.0" + }, { + "name": "advanced_network", + "version": "1.4" + }] + }, + "run": { + "command": "python3 /adv_networking_media_sender.py ../adv_networking_media_sender.yaml", + "workdir": "holohub_bin" + } + } +} From 8cb8e256cfb58fc1bbd61ea9a1d38126bf46b309 Mon Sep 17 00:00:00 2001 From: Rony Rado Date: Sun, 29 Jun 2025 21:04:45 +0000 Subject: [PATCH 3/6] feat(adv_networking_media_player) Add Adv. Networking Media Player MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add high-performance media receiving application built on Advanced Network Media Rx operator - Utilizes AdvNetworkMediaRxOp for professional media reception over Rivermax - Integrates with Advanced Network Manager for optimized network resource management - SMPTE 2110 compliant for professional broadcast applications - Real-time reception and processing of media streams with ultra-low latency - Support flexible output modes and format handling - Real-time visualization using HolovizOp for live monitoring - File output capability for recording and analysis - Format conversion support for optimal display - Support multiple video formats and resolutions - RGB888, YUV420, NV12, RGBA and other common video formats - Configurable bit depths (8, 10, 12, 16-bit) - Multiple resolution support up to 4K and beyond - GPU acceleration with GPUDirect for zero-copy operations - Implement dual language support - C++ implementation for maximum performance - Python implementation for ease of use and development Pipeline: Network Interface → Advanced Network Manager → AdvNetworkMediaRxOp → [FormatConverter] → HolovizOp/FramesWriter This application demonstrates how to use the Advanced Network Media operators for professional-grade media reception in broadcast and media production environments requiring ultra-low latency and high throughput performance. Signed-off-by: Rony Rado --- applications/CMakeLists.txt | 3 + .../CMakeLists.txt | 20 + .../adv_networking_media_player/README.md | 484 ++++++++++++++++++ .../adv_networking_media_player.yaml | 101 ++++ .../cpp/CMakeLists.txt | 44 ++ .../cpp/adv_networking_media_player.cpp | 338 ++++++++++++ .../cpp/metadata.json | 39 ++ .../python/CMakeLists.txt | 30 ++ .../python/adv_networking_media_player.py | 227 ++++++++ .../python/metadata.json | 39 ++ 10 files changed, 1325 insertions(+) create mode 100755 applications/adv_networking_media_player/CMakeLists.txt create mode 100755 applications/adv_networking_media_player/README.md create mode 100755 applications/adv_networking_media_player/adv_networking_media_player.yaml create mode 100755 applications/adv_networking_media_player/cpp/CMakeLists.txt create mode 100755 applications/adv_networking_media_player/cpp/adv_networking_media_player.cpp create mode 100755 applications/adv_networking_media_player/cpp/metadata.json create mode 100755 applications/adv_networking_media_player/python/CMakeLists.txt create mode 100755 applications/adv_networking_media_player/python/adv_networking_media_player.py create mode 100755 applications/adv_networking_media_player/python/metadata.json diff --git a/applications/CMakeLists.txt b/applications/CMakeLists.txt index 5ff6f37c33..e6d544dfc7 100644 --- a/applications/CMakeLists.txt +++ b/applications/CMakeLists.txt @@ -17,6 +17,9 @@ add_holohub_application(adv_networking_bench DEPENDS OPERATORS advanced_network) +add_holohub_application(adv_networking_media_player DEPENDS + OPERATORS advanced_network advanced_network_media_rx) + add_holohub_application(adv_networking_media_sender DEPENDS OPERATORS advanced_network advanced_network_media_tx) diff --git a/applications/adv_networking_media_player/CMakeLists.txt b/applications/adv_networking_media_player/CMakeLists.txt new file mode 100755 index 0000000000..97e06a0b11 --- /dev/null +++ b/applications/adv_networking_media_player/CMakeLists.txt @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Add subdirectories +add_subdirectory(cpp) +if(HOLOHUB_BUILD_PYTHON) + add_subdirectory(python) +endif() diff --git a/applications/adv_networking_media_player/README.md b/applications/adv_networking_media_player/README.md new file mode 100755 index 0000000000..f0e6e330b6 --- /dev/null +++ b/applications/adv_networking_media_player/README.md @@ -0,0 +1,484 @@ +# Advanced Networking Media Player + +The Advanced Networking Media Player is a high-performance application for receiving and displaying media streams over advanced network infrastructure using NVIDIA's Rivermax SDK. This application demonstrates real-time media streaming capabilities with ultra-low latency and high throughput. + +## Overview + +This application showcases professional-grade media streaming over IP networks, utilizing NVIDIA's advanced networking technologies. It receives media streams using the SMPTE 2110 standard and can either display them in real-time or save them to disk for further processing. + +### Key Features + +- **High-Performance Streaming**: Receive media streams with minimal latency using Rivermax SDK +- **SMPTE 2110 Compliance**: Industry-standard media over IP protocol support +- **Flexible Output**: Choose between real-time visualization or file output +- **GPU Acceleration**: Leverage GPUDirect for zero-copy operations +- **Multiple Format Support**: RGB888, YUV420, NV12, and other common video formats +- **Header-Data Split**: Optimized memory handling for improved performance + +### Application Architecture + +The Advanced Networking Media Player implements a sophisticated multi-layer architecture for high-performance media streaming with hardware acceleration and zero-copy optimizations. + +#### Complete Application Data Flow + +```mermaid +graph TD + %% Network Hardware Layer + subgraph NET[" "] + direction TB + A["Network Interface
(ConnectX NIC)"] --> B["Rivermax Hardware
Acceleration"] + end + + %% Advanced Network Manager Layer (includes RDK Services) + subgraph MGR[" "] + direction TB + C{{"RDK Service
(IPO/RTP Receiver)"}} + C -->|settings_type: ipo_receiver| D["IPO Receiver Service
(rmax_ipo_receiver)"] + C -->|settings_type: rtp_receiver| E["RTP Receiver Service
(rmax_rtp_receiver)"] + D --> F["Direct Memory Access
(DMA to Pre-allocated Regions)"] + E --> F + F --> G["Memory Regions
(Data_RX_CPU + Data_RX_GPU)"] + G --> H["RivermaxMgr
(Advanced Network Manager)"] + H --> I["Burst Assembly
(Packet Pointers Only)"] + I --> J["AnoBurstsQueue
(Pointer Distribution)"] + end + + %% Media RX Operator Layer + subgraph OPS[" "] + direction TB + K["AdvNetworkMediaRxOp
(Packet-to-Frame Conversion)"] + K --> L{{"HDS Configuration
(Header-Data Split)"}} + L -->|hds: true| M["Headers: CPU Memory
Payloads: GPU Memory"] + L -->|hds: false| N["Headers+Payloads: CPU Memory
with RTP offset"] + M --> O["Frame Assembly
(Automatic Strategy Selection)"] + N --> O + O --> P{{"Output Format
Configuration"}} + P -->|output_format: video_buffer| Q["VideoBuffer Entity"] + P -->|output_format: tensor| R["Tensor Entity"] + end + + %% Application Layer + subgraph APP[" "] + direction TB + S["Media Player Application
(Python/C++)"] + S --> T{{"Output Mode
Configuration"}} + T -->|visualize: true| U["HolovizOp
(Real-time Display)"] + T -->|write_to_file: true| V["FramesWriter
(File Output)"] + end + + %% Layer Connections + B --> C + J --> K + Q --> S + R --> S + + %% Layer Labels + NET -.-> |"Network Hardware Layer"| NET + MGR -.-> |"Advanced Network Manager
(includes RDK Services)"| MGR + OPS -.-> |"Media RX Operator"| OPS + APP -.-> |"Application Layer"| APP + + %% Styling + classDef networkLayer fill:#e1f5fe,stroke:#01579b,stroke-width:2px + classDef managerLayer fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px + classDef operatorLayer fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px + classDef appLayer fill:#fff3e0,stroke:#ef6c00,stroke-width:2px + classDef configDecision fill:#f9f9f9,stroke:#333,stroke-width:2px + classDef hdsPath fill:#e8f5e8 + classDef memoryOpt fill:#ffebee + + %% Apply layer styling to subgraphs + class NET networkLayer + class MGR managerLayer + class OPS operatorLayer + class APP appLayer + + %% Individual node styling + class A,B networkLayer + class C,D,E,F,G,H,I,J managerLayer + class K,O operatorLayer + class S,U,V appLayer + class L,P,T configDecision + class M,N hdsPath +``` + +#### Simplified Application Pipeline + +``` +┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ Network Hardware│ -> │ Advanced Network│ -> │ Media RX Operator│ -> │ Application │ +│ Layer │ │ Manager + RDK │ │ Layer │ │ Layer │ +└─────────────────┘ └─────────────────┘ └──────────────────┘ └─────────────────┘ +``` + +#### Layered Architecture Overview + +The application implements a 4-layer architecture for high-performance media streaming, with clear separation of concerns: + +**🌐 Network Hardware Layer** +- ConnectX NIC with Rivermax hardware acceleration +- Direct memory access and hardware-based packet processing + +**🔄 Advanced Network Manager Layer** +- **RDK Services Context**: IPO/RTP receivers run within this layer +- Protocol handling and memory management via integrated RDK services +- Pre-allocated memory regions (CPU + GPU) +- Rivermax manager coordination and service orchestration +- Burst assembly and packet pointer management +- Queue distribution for operator consumption + +**🎬 Media RX Operator Layer** +- `AdvNetworkMediaRxOp`: Packet-to-frame conversion +- Automatic HDS detection and strategy optimization +- Frame assembly with GPU acceleration + +**📱 Application Layer** +- Media Player application (Python/C++) +- Output configuration (visualization or file storage) +- User interface and control logic + +**Key Features:** +- **Automatic Optimization**: Each layer automatically configures for optimal performance +- **Zero-Copy Operations**: GPU memory management minimizes data movement +- **Error Recovery**: Robust handling across all layers +- **Scalable Design**: Clean interfaces enable easy extension and testing + +## Requirements + +### Hardware Requirements +- Linux system (x86_64 or aarch64) +- NVIDIA NIC with ConnectX-6 or later chip +- NVIDIA GPU (for visualization and GPU acceleration) +- Sufficient network bandwidth for target media streams + +### Software Requirements +- NVIDIA Rivermax SDK +- NVIDIA GPU drivers +- MOFED drivers (5.8-1.0.1.1 or later) +- DOCA 2.7 or later (if using DOCA backend) +- System tuning as described in the [High Performance Networking tutorial](../../tutorials/high_performance_networking/README.md) + +## Build Instructions + +### Build Docker Image + +Build the Docker image and application with Rivermax support: + +**C++ version:** +```bash +./holohub build adv_networking_media_player --build-args="--target rivermax" --configure-args="-D ANO_MGR:STRING=rivermax" --language cpp +``` + +**Python version:** +```bash +./holohub build adv_networking_media_player --build-args="--target rivermax" --configure-args="-D ANO_MGR:STRING=rivermax" --language python +``` + +### Launch Container + +Launch the Rivermax-enabled container: + +**C++ version:** +```bash +./holohub run-container adv_networking_media_player --build-args="--target rivermax" --docker-opts="-u root --privileged -e DISPLAY=:99 -v /opt/mellanox/rivermax/rivermax.lic:/opt/mellanox/rivermax/rivermax.lic -w /workspace/holohub/build/adv_networking_media_player/applications/adv_networking_media_player/cpp" +``` + +**Python version:** +```bash +./holohub run-container adv_networking_media_player --build-args="--target rivermax" --docker-opts="-u root --privileged -e DISPLAY=:99 -v /opt/mellanox/rivermax/rivermax.lic:/opt/mellanox/rivermax/rivermax.lic -w /workspace/holohub/build/adv_networking_media_player/applications/adv_networking_media_player/python" +``` + +## Running the Application + +### Prerequisites + +Before running, ensure your environment is properly configured: + +```bash +# Update PYTHONPATH for Python applications +# Note: Run this command from the container's working directory +# (as set by -w flag in run-container command) +export PYTHONPATH=${PYTHONPATH}:/opt/nvidia/holoscan/python/lib:$PWD/../../../python/lib:$PWD + +# Ensure proper system configuration (run as root if needed) +# See High Performance Networking tutorial for system tuning +``` + +### Running on Headless Servers + +To run the application on a headless server and preview the video via VNC, start a virtual X server and VNC server: + +```bash +# Start virtual X server on display :99 +Xvfb :99 -screen 0 1920x1080x24 -ac & + +# Start VNC server for remote access +x11vnc -display :99 -forever -shared -rfbport 5900 -nopw -bg +``` + +After starting these services, you can connect to the server using any VNC client (e.g., TigerVNC, RealVNC) at `:5900` to view the video output. + +**Note**: The `-nopw` flag disables password authentication. For production environments, consider setting a password using the `-passwd` option for security. + +### C++ Application + +```bash +./adv_networking_media_player adv_networking_media_player.yaml +``` + +### Python Application + +```bash +python3 ./adv_networking_media_player.py adv_networking_media_player.yaml +``` + +## Configuration + +The application uses a YAML configuration file that defines the complete data flow pipeline. The configuration has three main sections: + +1. **Advanced Network Manager Configuration**: Network interfaces, memory regions, and RDK services +2. **Media RX Operator Configuration**: Video format, frame dimensions, and output settings +3. **Application Configuration**: Visualization and file output options + +> **📁 Example Configuration Files**: +> - `applications/adv_networking_media_player/adv_networking_media_player.yaml` + +> **For detailed configuration parameter documentation**, see: +> - [Advanced Network Operator Configuration](../../operators/advanced_network/README.md) - Network settings, memory regions, Rivermax RX/TX settings +> - [Advanced Network Media RX Operator Configuration](../../operators/advanced_network_media/README.md) - HDS configuration, memory architecture, copy strategies, output formats + +### Quick Reference: Key Parameters That Must Match + +Critical parameters must be consistent across configuration sections to ensure proper operation: + +| Parameter Category | Section 1 | Section 2 | Example Values | Required Match | +|-------------------|-----------|-----------|----------------|----------------| +| **Video Format** | `advanced_network_media_rx.video_format` | `media_player_config.input_format` | RGB888 / rgb888 | ✓ Must be compatible | +| **Interface** | `advanced_network_media_rx.interface_name` | `advanced_network.interfaces.address` | cc:00.1 | ✓ Must match exactly | +| **Queue ID** | `advanced_network_media_rx.queue_id` | `advanced_network.interfaces.rx.queues.id` | 0 | ✓ Must match exactly | +| **Memory Location** | `advanced_network_media_rx.memory_location` | Memory region types (`host`/`device`) | device | ✓ Should be consistent | +| **HDS Mode** | `advanced_network_media_rx.hds` | Memory region configuration | true/false | ✓ Must align with regions | +| **Frame Dimensions** | `advanced_network_media_rx.frame_width/height` | Sender configuration | 3840x2160 | ⚠️ Must match sender | + +> **⚠️ IMPORTANT: Configuration Parameter Consistency** +> +> Parameters across the three configuration sections must be consistent and properly matched: +> - **Video Format Matching**: `video_format` (Media RX) must match `input_format` (Application) +> - **Memory Buffer Sizing**: `buf_size` in memory regions depends on video format, resolution, and packet size +> - RTP headers: ~20 bytes per packet (CPU memory region) +> - Payload size: ~1440 bytes per packet for typical SMPTE 2110 streams (GPU memory region) +> - Buffer count (`num_bufs`): Must accommodate all packets for multiple frames in flight +> - **Memory Location**: `memory_location` (Media RX) should match the memory region types configured (`host` vs `device`) +> - **HDS Configuration**: When `hds: true`, ensure both CPU and GPU memory regions are configured +> - **Interface Matching**: `interface_name` (Media RX) must match the interface address/name in Advanced Network config +> +> Mismatched parameters will result in runtime errors or degraded performance. + +### Configuration File Structure + +The application configuration consists of three main sections: + +#### 1. Advanced Network Manager Configuration + +Configures network interfaces, memory regions, and Rivermax RX settings. See [Advanced Network Operator documentation](../../operators/advanced_network/README.md) for detailed parameter descriptions. + +```yaml +advanced_network: + cfg: + version: 1 + manager: "rivermax" + master_core: 6 + debug: 1 + log_level: "error" + memory_regions: + - name: "Data_RX_CPU" + kind: "host" + affinity: 0 + access: + - local + num_bufs: 432000 + buf_size: 20 + - name: "Data_RX_GPU" + kind: "device" + affinity: 0 + access: + - local + num_bufs: 432000 + buf_size: 1440 + interfaces: + - name: data1 + address: cc:00.1 + rx: + queues: + - name: "Data" + id: 0 + cpu_core: "12" + batch_size: 4320 + output_port: "bench_rx_out_1" + memory_regions: + - "Data_RX_CPU" + - "Data_RX_GPU" + rivermax_rx_settings: + settings_type: "ipo_receiver" + memory_registration: true + verbose: true + max_path_diff_us: 10000 + ext_seq_num: true + sleep_between_operations_us: 0 + local_ip_addresses: + - 2.1.0.12 + source_ip_addresses: + - 2.1.0.12 + destination_ip_addresses: + - 224.1.1.2 + destination_ports: + - 50001 + stats_report_interval_ms: 3000 + send_packet_ext_info: true +``` + +**Key Rivermax RX Settings**: +- `settings_type: "ipo_receiver"` - Uses IPO (Inline Packet Ordering) for high-throughput streams; alternatively use `"rtp_receiver"` for standard RTP +- `memory_registration: true` - Registers memory with Rivermax for DMA operations +- `local_ip_addresses` - Local interface IP addresses for receiving streams +- `source_ip_addresses` - Expected source IP addresses (can match local for loopback testing) +- `destination_ip_addresses` - Destination IP addresses (multicast: 224.0.0.0 - 239.255.255.255) +- `destination_ports` - UDP ports to receive streams on +- `ext_seq_num: true` - Enables extended sequence number support for SMPTE 2110 +- `send_packet_ext_info: true` - Provides extended packet information to operators + +**Memory Regions for HDS (Header-Data Split)**: +- `Data_RX_CPU` (kind: host) - CPU memory for RTP headers (20 bytes per packet) +- `Data_RX_GPU` (kind: device) - GPU memory for video payloads (1440 bytes per packet for 1080p RGB) + +#### 2. Media RX Operator Configuration + +Configures video format, frame dimensions, HDS, and output settings. See [Advanced Network Media RX Operator documentation](../../operators/advanced_network_media/README.md) for detailed parameter descriptions. + +```yaml +advanced_network_media_rx: + interface_name: cc:00.1 # Must match Advanced Network interface address + queue_id: 0 # Must match Advanced Network queue ID + video_format: RGB888 # Video pixel format (RGB888, YUV420, NV12) + frame_width: 1920 # Frame width in pixels (must match sender) + frame_height: 1080 # Frame height in pixels (must match sender) + bit_depth: 8 # Color bit depth (8, 10, 12, 16) + hds: true # Enable Header-Data Split for optimal GPU performance + output_format: tensor # Output as tensor (alternative: video_buffer) + memory_location: device # Process in GPU memory (alternative: host) +``` + +**Key Media RX Operator Settings**: +- `interface_name` - Must match the interface address in Advanced Network configuration +- `queue_id` - Must match the queue ID in Advanced Network RX queue configuration +- `video_format` - Must be compatible with sender format and `media_player_config.input_format` +- `frame_width` / `frame_height` - Must match sender configuration (example shows 1080p: 1920x1080) +- `hds: true` - Enables Header-Data Split: headers in CPU, payloads in GPU (requires both CPU and GPU memory regions) +- `output_format: tensor` - Optimized for GPU post-processing; use `video_buffer` for compatibility with standard operators +- `memory_location: device` - Process frames in GPU memory for maximum performance + +#### 3. Application Configuration + +Configures output options for the media player application: + +```yaml +media_player_config: + write_to_file: false # Enable file output (saves frames to disk) + visualize: true # Enable real-time display via HolovizOp + input_format: "rgb888" # Must be compatible with advanced_network_media_rx.video_format +``` + +**Key Application Settings**: +- `write_to_file` - When true, saves received frames to disk (configure path in `frames_writer` section) +- `visualize` - When true, displays frames in real-time using HolovizOp (requires X11/display) +- `input_format` - Must be compatible with Media RX Operator `video_format` (e.g., RGB888 → rgb888) + +## Output Options + +### Real-time Visualization + +When `visualize: true` is set in the configuration: +- Received media streams are displayed in real-time using HolovizOp +- Supports format conversion for optimal display +- Minimal latency for live monitoring applications + +### File Output + +When `write_to_file: true` is set in the configuration: +- Media frames are saved to disk for later analysis +- Configure output path and number of frames in the `frames_writer` section +- Supports both host and device memory sources + +## Supported Video Formats + +- **RGB888**: 24-bit RGB color +- **YUV420**: 4:2:0 chroma subsampling +- **NV12**: Semi-planar YUV 4:2:0 + +## Troubleshooting + +### Common Issues + +1. **Permission Errors**: Run with appropriate privileges for network interface access +2. **Network Configuration**: Verify IP addresses, ports, and interface names +3. **Memory Issues**: Adjust buffer sizes based on available system memory +4. **Performance**: Check system tuning and CPU isolation settings + +### Debug Options + +Enable debug logging by setting `log_level: "debug"` in the advanced_network configuration section. + +### Git Configuration (if needed) + +If you encounter Git-related issues during build: + +```bash +git config --global --add safe.directory '*' +``` + +## Performance Optimization + +### Configuration Optimization + +For optimal performance, configure the following based on your use case: + +1. **HDS Configuration**: Enable `hds: true` for GPU-accelerated processing with optimal memory layout +2. **Memory Location**: Set `memory_location: device` to process frames in GPU memory for maximum performance +3. **Output Format**: Use `output_format: tensor` for GPU-based post-processing pipelines +4. **Memory Regions**: Size buffers appropriately for your frame rate and resolution + +> **For detailed optimization strategies and implementation details**, see: +> - [Advanced Network Media RX Operator Documentation](../../operators/advanced_network_media/README.md) - Processing strategies, HDS optimization, memory architecture +> - [Advanced Network Operator Documentation](../../operators/advanced_network/README.md) - Memory region configuration, queue settings + +### System-Level Tuning + +Ensure your system is properly configured for high-performance networking: + +- **CPU Isolation**: Isolate CPU cores for network processing +- **Memory Configuration**: Configure hugepages and memory allocation +- **GPU Configuration**: Ensure sufficient GPU memory for frame buffering +- **Network Tuning**: Configure interrupt mitigation and CPU affinity + +> **For detailed system tuning guidelines**, see the [High Performance Networking Tutorial](../../tutorials/high_performance_networking/README.md) + +### Performance Monitoring + +Monitor these key metrics for optimal performance: +- **Frame Rate**: Consistent frame reception without drops +- **Processing Efficiency**: Verify the operator is selecting optimal processing strategies +- **GPU Utilization**: Monitor GPU memory usage and processing efficiency +- **Network Statistics**: Track packet loss, timing accuracy, and throughput +- **Memory Usage**: Monitor both CPU and GPU memory consumption +- **Error Recovery**: Track frequency of network issues and recovery events + +## Related Documentation + +### Operator Documentation +For detailed implementation information and advanced configuration: +- **[Advanced Network Media RX Operator](../../operators/advanced_network_media/README.md)**: Comprehensive operator documentation and configuration options +- **[Advanced Network Operators](../../operators/advanced_network/README.md)**: Base networking infrastructure and setup + +### Additional Resources +- **[High Performance Networking Tutorial](../../tutorials/high_performance_networking/README.md)**: System tuning and optimization guide +- **[Advanced Networking Media Sender](../adv_networking_media_sender/README.md)**: Companion application for media transmission diff --git a/applications/adv_networking_media_player/adv_networking_media_player.yaml b/applications/adv_networking_media_player/adv_networking_media_player.yaml new file mode 100755 index 0000000000..051886f1b0 --- /dev/null +++ b/applications/adv_networking_media_player/adv_networking_media_player.yaml @@ -0,0 +1,101 @@ +%YAML 1.2 +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +scheduler: + check_recession_period_ms: 0 + worker_thread_number: 8 + stop_on_deadlock: true + stop_on_deadlock_timeout: 500 + +advanced_network: + cfg: + version: 1 + manager: "rivermax" + master_core: 6 # Master CPU core + debug: 1 + log_level: "error" + + memory_regions: + - name: "Data_RX_CPU" + kind: "host" + affinity: 0 + access: + - local + num_bufs: 432000 + buf_size: 20 + - name: "Data_RX_GPU" + kind: "device" + affinity: 0 + access: + - local + num_bufs: 432000 + buf_size: 1440 + interfaces: + - name: data1 + address: cc:00.1 + rx: + queues: + - name: "Data" + id: 0 + cpu_core: "12" + batch_size: 4320 + output_port: "bench_rx_out_1" + memory_regions: + - "Data_RX_CPU" + - "Data_RX_GPU" + rivermax_rx_settings: + settings_type: "ipo_receiver" + memory_registration: true + #allocator_type: "huge_page_2mb" + verbose: true + max_path_diff_us: 10000 + ext_seq_num: true + sleep_between_operations_us: 0 + local_ip_addresses: + - 2.1.0.12 + source_ip_addresses: + - 2.1.0.12 + destination_ip_addresses: + - 224.1.1.2 + destination_ports: + - 50001 + stats_report_interval_ms: 3000 + send_packet_ext_info: true + +advanced_network_media_rx: + interface_name: cc:00.1 + queue_id: 0 + video_format: RGB888 + frame_width: 1920 + frame_height: 1080 + bit_depth: 8 + hds: true + output_format: tensor + memory_location: device + +media_player_config: + write_to_file: false + visualize: true + input_format: "rgb888" + +frames_writer: # applied only to cpp + num_of_frames_to_record: 1000 + file_path: "/tmp/output.bin" + +holoviz: + width: 1280 + height: 720 + \ No newline at end of file diff --git a/applications/adv_networking_media_player/cpp/CMakeLists.txt b/applications/adv_networking_media_player/cpp/CMakeLists.txt new file mode 100755 index 0000000000..b9dddab43b --- /dev/null +++ b/applications/adv_networking_media_player/cpp/CMakeLists.txt @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required(VERSION 3.20) +project(adv_networking_media_player CXX) + +# Dependencies +find_package(holoscan 2.6 REQUIRED CONFIG + PATHS "/opt/nvidia/holoscan" "/workspace/holoscan-sdk/install") + +# Global variables +set(CMAKE_CUDA_ARCHITECTURES "70;80;90") + +# Create the executable +add_executable(${PROJECT_NAME} + adv_networking_media_player.cpp +) + +target_link_libraries(${PROJECT_NAME} + PRIVATE + holoscan::core + holoscan::advanced_network + holoscan::ops::advanced_network_media_rx + holoscan::ops::holoviz +) + +# Copy config file +add_custom_target(adv_networking_media_player_yaml + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/../adv_networking_media_player.yaml" ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/../adv_networking_media_player.yaml" +) +add_dependencies(${PROJECT_NAME} adv_networking_media_player_yaml) diff --git a/applications/adv_networking_media_player/cpp/adv_networking_media_player.cpp b/applications/adv_networking_media_player/cpp/adv_networking_media_player.cpp new file mode 100755 index 0000000000..438a6e7239 --- /dev/null +++ b/applications/adv_networking_media_player/cpp/adv_networking_media_player.cpp @@ -0,0 +1,338 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include "holoscan/holoscan.hpp" +#include "gxf/core/gxf.h" +#include "holoscan/operators/holoviz/holoviz.hpp" +#include "advanced_network/common.h" +#include "adv_network_media_rx.h" + +namespace ano = holoscan::advanced_network; +using holoscan::advanced_network::NetworkConfig; +using holoscan::advanced_network::Status; + +#define CUDA_TRY(stmt) \ + { \ + cudaError_t cuda_status = stmt; \ + if (cudaSuccess != cuda_status) { \ + HOLOSCAN_LOG_ERROR("Runtime call {} in line {} of file {} failed with '{}' ({})", \ + #stmt, \ + __LINE__, \ + __FILE__, \ + cudaGetErrorString(cuda_status), \ + static_cast(cuda_status)); \ + throw std::runtime_error("CUDA operation failed"); \ + } \ + } + +namespace holoscan::ops { + +/** + * @class FramesWriterOp + * @brief Operator for writing frame data to file. + * + * This operator can handle: + * - Input types: VideoBuffer, GXF Tensor + * - Memory sources: Host memory (kHost, kSystem) and Device memory (kDevice) + * - Automatic memory type detection and appropriate copying (device-to-host when needed) + * + * Features: + * - Automatic input type detection (VideoBuffer takes precedence, then GXF Tensor) + * - Efficient memory handling with reusable host buffer for device-to-host copies + * - Comprehensive error handling and logging + * - Binary file output with proper stream management + * + * Parameters: + * - num_of_frames_to_record: Number of frames to write before stopping + * - file_path: Output file path (default: "./output.bin") + */ +class FramesWriterOp : public Operator { + public: + HOLOSCAN_OPERATOR_FORWARD_ARGS(FramesWriterOp) + + FramesWriterOp() = default; + + ~FramesWriterOp() { + if (file_stream_.is_open()) { file_stream_.close(); } + } + + void setup(OperatorSpec& spec) override { + spec.input("input"); + spec.param(num_of_frames_to_record_, + "num_of_frames_to_record", + "The number of frames to write to file"); + spec.param( + file_path_, "file_path", "Output File Path", "Path to the output file", "./output.bin"); + } + + void initialize() override { + HOLOSCAN_LOG_INFO("FramesWriterOp::initialize()"); + holoscan::Operator::initialize(); + + std::string file_path = file_path_.get(); + HOLOSCAN_LOG_INFO("Original file path from config: {}", file_path); + HOLOSCAN_LOG_INFO("Current working directory: {}", std::filesystem::current_path().string()); + + // Convert to absolute path if relative + std::filesystem::path path(file_path); + if (!path.is_absolute()) { + path = std::filesystem::absolute(path); + file_path = path.string(); + HOLOSCAN_LOG_INFO("Converted to absolute path: {}", file_path); + } + + HOLOSCAN_LOG_INFO("Attempting to open output file: {}", file_path); + file_stream_.open(file_path, std::ios::out | std::ios::binary); + if (!file_stream_) { + HOLOSCAN_LOG_ERROR("Failed to open output file: {}. Check permissions and disk space.", + file_path); + + // Additional debugging information + std::error_code ec; + if (path.has_parent_path()) { + auto file_status = std::filesystem::status(path.parent_path(), ec); + if (!ec) { + auto perms = file_status.permissions(); + HOLOSCAN_LOG_ERROR("Parent directory permissions: {}", static_cast(perms)); + } + } + + throw std::runtime_error("Failed to open output file: " + file_path); + } + + HOLOSCAN_LOG_INFO("Successfully opened output file: {}", file_path); + } + + void compute(InputContext& op_input, [[maybe_unused]] OutputContext&, + ExecutionContext& context) override { + auto maybe_entity = op_input.receive("input"); + if (!maybe_entity) { throw std::runtime_error("Failed to receive input"); } + + auto& entity = static_cast(maybe_entity.value()); + + if (frames_recorded_ >= num_of_frames_to_record_.get()) { return; } + + auto maybe_video_buffer = entity.get(); + if (maybe_video_buffer) { + process_video_buffer(maybe_video_buffer.value()); + } else { + auto maybe_tensor = entity.get(); + if (!maybe_tensor) { + HOLOSCAN_LOG_ERROR("Neither VideoBuffer nor Tensor found in message"); + return; + } + process_gxf_tensor(maybe_tensor.value()); + } + + frames_recorded_++; + } + + private: + /** + * @brief Processes a VideoBuffer input and writes its data to file. + * + * Extracts data from the VideoBuffer, determines memory storage type, + * and calls write_data_to_file to handle the actual file writing. + * + * @param video_buffer Handle to the GXF VideoBuffer to process + */ + void process_video_buffer(nvidia::gxf::Handle video_buffer) { + const auto buffer_size = video_buffer->size(); + const auto storage_type = video_buffer->storage_type(); + const auto data_ptr = video_buffer->pointer(); + + HOLOSCAN_LOG_TRACE("Processing VideoBuffer: size={}, storage_type={}", + buffer_size, + static_cast(storage_type)); + + write_data_to_file(data_ptr, buffer_size, storage_type); + } + + /** + * @brief Processes a GXF Tensor input and writes its data to file. + * + * Extracts data from the GXF Tensor, determines memory storage type, + * and calls write_data_to_file to handle the actual file writing. + * + * @param tensor Handle to the GXF Tensor to process + */ + void process_gxf_tensor(nvidia::gxf::Handle tensor) { + const auto tensor_size = tensor->size(); + const auto storage_type = tensor->storage_type(); + const auto data_ptr = tensor->pointer(); + + HOLOSCAN_LOG_TRACE( + "Processing Tensor: size={}, storage_type={}", tensor_size, static_cast(storage_type)); + + write_data_to_file(data_ptr, tensor_size, storage_type); + } + + /** + * @brief Writes data to file, handling both host and device memory sources. + * + * For host memory (kHost, kSystem), writes data directly to file. + * For device memory (kDevice), copies data to host buffer first, then writes to file. + * Automatically resizes the host buffer as needed and includes comprehensive error checking. + * + * @param data_ptr Pointer to the data to write + * @param data_size Size of the data in bytes + * @param storage_type Memory storage type indicating where the data resides + * + * @throws std::runtime_error If file stream is in bad state, CUDA operations fail, + * or unsupported memory storage type is encountered + */ + void write_data_to_file(void* data_ptr, size_t data_size, + nvidia::gxf::MemoryStorageType storage_type) { + if (!data_ptr || data_size == 0) { + HOLOSCAN_LOG_ERROR( + "Invalid data pointer or size: ptr={}, size={}", static_cast(data_ptr), data_size); + return; + } + + if (!file_stream_.is_open() || !file_stream_.good()) { + HOLOSCAN_LOG_ERROR("File stream is not open or in bad state"); + throw std::runtime_error("File stream error"); + } + + // Ensure host buffer is large enough + if (host_buffer_.size() < data_size) { + HOLOSCAN_LOG_TRACE( + "Resizing host buffer from {} to {} bytes", host_buffer_.size(), data_size); + host_buffer_.resize(data_size); + } + + switch (storage_type) { + case nvidia::gxf::MemoryStorageType::kHost: + case nvidia::gxf::MemoryStorageType::kSystem: { + // Data is already on host, write directly + file_stream_.write(reinterpret_cast(data_ptr), data_size); + break; + } + case nvidia::gxf::MemoryStorageType::kDevice: { + // Data is on device, copy to host first + CUDA_TRY(cudaMemcpy(host_buffer_.data(), data_ptr, data_size, cudaMemcpyDeviceToHost)); + file_stream_.write(reinterpret_cast(host_buffer_.data()), data_size); + break; + } + default: { + HOLOSCAN_LOG_ERROR("Unsupported memory storage type: {}", static_cast(storage_type)); + throw std::runtime_error("Unsupported memory storage type"); + } + } + + if (!file_stream_.good()) { + HOLOSCAN_LOG_ERROR("Failed to write data to file - stream state: fail={}, bad={}, eof={}", + file_stream_.fail(), + file_stream_.bad(), + file_stream_.eof()); + throw std::runtime_error("Failed to write data to file"); + } + + // Flush to ensure data is written + file_stream_.flush(); + + HOLOSCAN_LOG_TRACE( + "Successfully wrote {} bytes to file (frame {})", data_size, frames_recorded_ + 1); + } + + std::ofstream file_stream_; + std::vector host_buffer_; // Buffer for device-to-host copies + uint32_t frames_recorded_ = 0; + Parameter num_of_frames_to_record_; + Parameter file_path_; +}; + +} // namespace holoscan::ops + +class App : public holoscan::Application { + public: + void compose() override { + using namespace holoscan; + + auto adv_net_config = from_config("advanced_network").as(); + if (ano::adv_net_init(adv_net_config) != Status::SUCCESS) { + HOLOSCAN_LOG_ERROR("Failed to configure the Advanced Network manager"); + exit(1); + } + HOLOSCAN_LOG_INFO("Configured the Advanced Network manager"); + + const auto [rx_en, tx_en] = ano::get_rx_tx_configs_enabled(config()); + const auto mgr_type = ano::get_manager_type(config()); + + HOLOSCAN_LOG_INFO("Using Advanced Network manager {}", + ano::manager_type_to_string(mgr_type)); + if (!rx_en) { + HOLOSCAN_LOG_ERROR("Rx is not enabled. Please enable Rx in the config file."); + exit(1); + } + + auto adv_net_media_rx = + make_operator("advanced_network_media_rx", + from_config("advanced_network_media_rx"), + make_condition("is_alive", true)); + + const auto allocator = make_resource("allocator"); + + if (from_config("media_player_config.visualize").as()) { + const auto cuda_stream_pool = + make_resource("cuda_stream", 0, 0, 0, 1, 5); + + auto visualizer = make_operator("visualizer", + from_config("holoviz"), + Arg("cuda_stream_pool", cuda_stream_pool), + Arg("allocator") = allocator); + add_flow(adv_net_media_rx, visualizer, {{"out_video_buffer", "receivers"}}); + } else if (from_config("media_player_config.write_to_file").as()) { + auto frames_writer = + make_operator("frames_writer", from_config("frames_writer")); + add_flow(adv_net_media_rx, frames_writer); + } else { + HOLOSCAN_LOG_ERROR("At least one output type (write_to_file/visualize) must be defined"); + exit(1); + } + } +}; + +int main(int argc, char** argv) { + using namespace holoscan; + auto app = holoscan::make_application(); + + // Get the configuration + if (argc < 2) { + HOLOSCAN_LOG_ERROR("Usage: {} config_file", argv[0]); + return -1; + } + + std::filesystem::path config_path(argv[1]); + if (!config_path.is_absolute()) { + config_path = std::filesystem::canonical(argv[0]).parent_path() / config_path; + } + + app->config(config_path); + app->scheduler(app->make_scheduler("multithread-scheduler", + app->from_config("scheduler"))); + app->run(); + + ano::shutdown(); + + return 0; +} diff --git a/applications/adv_networking_media_player/cpp/metadata.json b/applications/adv_networking_media_player/cpp/metadata.json new file mode 100755 index 0000000000..2e89095044 --- /dev/null +++ b/applications/adv_networking_media_player/cpp/metadata.json @@ -0,0 +1,39 @@ +{ + "application": { + "name": "Advanced Networking Media Player", + "authors": [ + { + "name": "Rony Rado", + "affiliation": "NVIDIA" + } + ], + "language": "C++", + "version": "1.0", + "changelog": { + "1.0": "Initial Release" + }, + "platforms": ["x86_64", "aarch64"], + "tags": ["Networking and Distributed Computing", "UDP", "Ethernet", "IP", "GPUDirect", "Rivermax"], + "dockerfile": "operators/advanced_network/Dockerfile", + "holoscan_sdk": { + "minimum_required_version": "2.6.0", + "tested_versions": [ + "2.6.0" + ] + }, + "ranking": 3, + "requirements": { + "operators": [{ + "name": "advanced_network_media_rx", + "version": "1.0" + }, { + "name": "advanced_network", + "version": "1.4" + }] + }, + "run": { + "command": "/adv_networking_media_player adv_networking_media_player.yaml", + "workdir": "holohub_bin" + } + } +} diff --git a/applications/adv_networking_media_player/python/CMakeLists.txt b/applications/adv_networking_media_player/python/CMakeLists.txt new file mode 100755 index 0000000000..6cc7ed9a1a --- /dev/null +++ b/applications/adv_networking_media_player/python/CMakeLists.txt @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copy adv_networking_media_player application file +add_custom_target(python_adv_networking_media_player ALL + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/adv_networking_media_player.py" ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS "adv_networking_media_player.py" + BYPRODUCTS "adv_networking_media_player.py" +) + +# Copy config file +add_custom_target(python_adv_networking_media_player_yaml + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/../adv_networking_media_player.yaml" ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS "../adv_networking_media_player.yaml" + BYPRODUCTS "adv_networking_media_player.yaml" +) + +add_dependencies(python_adv_networking_media_player python_adv_networking_media_player_yaml) diff --git a/applications/adv_networking_media_player/python/adv_networking_media_player.py b/applications/adv_networking_media_player/python/adv_networking_media_player.py new file mode 100755 index 0000000000..b05cb3867a --- /dev/null +++ b/applications/adv_networking_media_player/python/adv_networking_media_player.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import sys +from pathlib import Path + +from holoscan.core import Application +from holoscan.operators.holoviz import HolovizOp +from holoscan.resources import CudaStreamPool, UnboundedAllocator +from holoscan.schedulers import MultiThreadScheduler + +from holohub.advanced_network_common import _advanced_network_common as adv_network_common +from holohub.advanced_network_media_rx import _advanced_network_media_rx as adv_network_media_rx + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def check_rx_tx_enabled(app, require_rx=True, require_tx=False): + """ + Check if RX and TX are enabled in the advanced network configuration. + + Args: + app: The Holoscan Application instance + require_rx: Whether RX must be enabled (default: True) + require_tx: Whether TX must be enabled (default: False) + + Returns: + tuple: (rx_enabled, tx_enabled) + + Raises: + SystemExit: If required functionality is not enabled + """ + try: + adv_net_config_dict = app.kwargs("advanced_network") + + rx_enabled = False + tx_enabled = False + + # Check if there are interfaces with RX/TX configurations + if "cfg" in adv_net_config_dict and "interfaces" in adv_net_config_dict["cfg"]: + for interface in adv_net_config_dict["cfg"]["interfaces"]: + if "rx" in interface: + rx_enabled = True + if "tx" in interface: + tx_enabled = True + + logger.info(f"RX enabled: {rx_enabled}, TX enabled: {tx_enabled}") + + if require_rx and not rx_enabled: + logger.error("RX is not enabled. Please enable RX in the config file.") + sys.exit(1) + + if require_tx and not tx_enabled: + logger.error("TX is not enabled. Please enable TX in the config file.") + sys.exit(1) + + return rx_enabled, tx_enabled + + except Exception as e: + logger.warning(f"Could not check RX/TX status from advanced_network config: {e}") + # Fallback: check if we have the required operator configs + try: + if require_rx: + app.from_config("advanced_network_media_rx") + logger.info("RX is enabled (found advanced_network_media_rx config)") + if require_tx: + app.from_config("advanced_network_media_tx") + logger.info("TX is enabled (found advanced_network_media_tx config)") + return require_rx, require_tx + except Exception as e2: + if require_rx: + logger.error("RX is not enabled. Please enable RX in the config file.") + logger.error(f"Could not find advanced_network_media_rx configuration: {e2}") + sys.exit(1) + if require_tx: + logger.error("TX is not enabled. Please enable TX in the config file.") + logger.error(f"Could not find advanced_network_media_tx configuration: {e2}") + sys.exit(1) + return False, False + + +class App(Application): + def compose(self): + # Initialize advanced network + try: + adv_net_config = self.from_config("advanced_network") + if adv_network_common.adv_net_init(adv_net_config) != adv_network_common.Status.SUCCESS: + logger.error("Failed to configure the Advanced Network manager") + sys.exit(1) + logger.info("Configured the Advanced Network manager") + except Exception as e: + logger.error(f"Failed to get advanced network config or initialize: {e}") + sys.exit(1) + + # Get manager type + try: + mgr_type = adv_network_common.get_manager_type() + logger.info( + f"Using Advanced Network manager {adv_network_common.manager_type_to_string(mgr_type)}" + ) + except Exception as e: + logger.warning(f"Could not get manager type: {e}") + + # Check RX/TX enabled status (require RX for media player) + check_rx_tx_enabled(self, require_rx=True, require_tx=False) + logger.info("RX is enabled, proceeding with application setup") + + allocator = UnboundedAllocator(self, name="allocator") + + # Create shared CUDA stream pool for format converters and CUDA operations + # Optimized sizing for video processing workloads + cuda_stream_pool = CudaStreamPool( + self, + name="cuda_stream_pool", + dev_id=0, + stream_flags=0, + stream_priority=0, + reserved_size=1, + max_size=5, + ) + + try: + rx_config = self.kwargs("advanced_network_media_rx") + + adv_net_media_rx = adv_network_media_rx.AdvNetworkMediaRxOp( + fragment=self, + **rx_config, + name="advanced_network_media_rx", + ) + + except Exception as e: + logger.error(f"Failed to create AdvNetworkMediaRxOp: {e}") + sys.exit(1) + + # Set up visualization pipeline + try: + # Create visualizer + holoviz_config = self.kwargs("holoviz") + visualizer = HolovizOp( + fragment=self, + name="visualizer", + allocator=allocator, + cuda_stream_pool=cuda_stream_pool, + **holoviz_config, + ) + + self.add_flow(adv_net_media_rx, visualizer, {("out_video_buffer", "receivers")}) + + except Exception as e: + logger.error(f"Failed to set up visualization pipeline: {e}") + sys.exit(1) + + # Set up scheduler + try: + scheduler_config = self.kwargs("scheduler") + scheduler = MultiThreadScheduler( + fragment=self, name="multithread-scheduler", **scheduler_config + ) + self.scheduler(scheduler) + except Exception as e: + logger.error(f"Failed to set up scheduler: {e}") + sys.exit(1) + + logger.info("Application composition completed successfully") + + +def main(): + if len(sys.argv) < 2: + logger.error(f"Usage: {sys.argv[0]} config_file") + sys.exit(1) + + config_path = Path(sys.argv[1]) + + # Convert to absolute path if relative + if not config_path.is_absolute(): + # Get the directory of the script and make path relative to it + script_dir = Path(sys.argv[0]).parent.resolve() + config_path = script_dir / config_path + + if not config_path.exists(): + logger.error(f"Config file not found: {config_path}") + sys.exit(1) + + logger.info(f"Using config file: {config_path}") + + try: + app = App() + app.config(str(config_path)) + + logger.info("Starting application...") + app.run() + + logger.info("Application finished") + + except Exception as e: + logger.error(f"Application failed: {e}") + import traceback + + traceback.print_exc() + sys.exit(1) + finally: + # Shutdown advanced network + try: + adv_network_common.shutdown() + logger.info("Advanced Network shutdown completed") + except Exception as e: + logger.warning(f"Error during advanced network shutdown: {e}") + + +if __name__ == "__main__": + main() diff --git a/applications/adv_networking_media_player/python/metadata.json b/applications/adv_networking_media_player/python/metadata.json new file mode 100755 index 0000000000..c92b625469 --- /dev/null +++ b/applications/adv_networking_media_player/python/metadata.json @@ -0,0 +1,39 @@ +{ + "application": { + "name": "Advanced Networking Media Player", + "authors": [ + { + "name": "Rony Rado", + "affiliation": "NVIDIA" + } + ], + "language": "Python", + "version": "1.0", + "changelog": { + "1.0": "Initial Release" + }, + "platforms": ["x86_64", "aarch64"], + "tags": ["Networking and Distributed Computing", "UDP", "Ethernet", "IP", "GPUDirect", "Rivermax"], + "dockerfile": "operators/advanced_network/Dockerfile", + "holoscan_sdk": { + "minimum_required_version": "2.6.0", + "tested_versions": [ + "2.6.0" + ] + }, + "ranking": 3, + "requirements": { + "operators": [{ + "name": "advanced_network_media_rx", + "version": "1.0" + }, { + "name": "advanced_network", + "version": "1.4" + }] + }, + "run": { + "command": "python3 /adv_networking_media_player.py ../adv_networking_media_player.yaml", + "workdir": "holohub_bin" + } + } +} From b1ba964372c9e1b25c89c8267eb8dcc67bc34d42 Mon Sep 17 00:00:00 2001 From: Rony Rado Date: Wed, 29 Oct 2025 17:32:29 +0200 Subject: [PATCH 4/6] feat(advanced_network) Implement adaptive burst pool management with configurable thresholds - Add adaptive burst dropping system for Rivermax burst pool management - Implements capacity-aware burst dropping to prevent memory pool exhaustion - Configurable thresholds for warning (25%), critical (10%), and recovery (50%) levels - Critical threshold mode drops new bursts when pool capacity falls below threshold - Automatic recovery when pool capacity returns to healthy levels - Add comprehensive pool capacity monitoring and statistics - Extend YAML configuration support for burst pool adaptive dropping - New "burst_pool_adaptive_dropping" configuration section - Configurable enabled/disabled flag and threshold percentages This enhancement prevents memory pool exhaustion in high-throughput ANO Rivermax operations by intelligently dropping incoming bursts when system capacity is critically low, ensuring system stability while maintaining optimal performance under normal operating conditions. Signed-off-by: Rony Rado --- .../adv_network_rivermax_mgr.cpp | 33 ++++ .../rivermax_mgr_impl/burst_manager.cpp | 168 +++++++++++++++++- .../rivermax_mgr_impl/burst_manager.h | 163 ++++++++++++++++- .../rivermax_config_manager.cpp | 66 +++++++ .../rivermax_mgr_service.cpp | 44 ++++- .../rivermax_mgr_impl/rivermax_mgr_service.h | 22 +++ .../rivermax_queue_configs.cpp | 23 ++- .../rivermax_queue_configs.h | 20 +++ 8 files changed, 534 insertions(+), 5 deletions(-) diff --git a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/adv_network_rivermax_mgr.cpp b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/adv_network_rivermax_mgr.cpp index f885e163ee..fbf04abb5c 100644 --- a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/adv_network_rivermax_mgr.cpp +++ b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/adv_network_rivermax_mgr.cpp @@ -131,6 +131,7 @@ class RivermaxMgr::RivermaxMgrImpl { Status get_mac_addr(int port, char* mac); private: + void apply_burst_pool_configuration_to_service(uint32_t service_id); static void flush_packets(int port); void setup_accurate_send_scheduling_mask(); int setup_pools_and_rings(int max_rx_batch, int max_tx_batch); @@ -225,6 +226,7 @@ void RivermaxMgr::RivermaxMgrImpl::initialize() { burst_tx_pool[i].hdr.hdr.port_id = 0; burst_tx_pool[i].hdr.hdr.q_id = 0; burst_tx_pool[i].hdr.hdr.num_pkts = MAX_NUM_OF_FRAMES_IN_BURST; + burst_tx_pool[i].hdr.hdr.burst_flags = FLAGS_NONE; burst_tx_pool[i].pkts[0] = new void*[MAX_NUM_OF_FRAMES_IN_BURST]; burst_tx_pool[i].pkt_lens[0] = new uint32_t[MAX_NUM_OF_FRAMES_IN_BURST]; burst_tx_pool[i].pkt_extra_info = new void*[MAX_NUM_OF_FRAMES_IN_BURST]; @@ -299,6 +301,10 @@ bool RivermaxMgr::RivermaxMgrImpl::initialize_rx_service( } rx_services_[service_id] = std::move(rx_service); + + // Apply burst pool adaptive dropping configuration + apply_burst_pool_configuration_to_service(service_id); + return true; } @@ -601,6 +607,33 @@ Status RivermaxMgr::RivermaxMgrImpl::get_mac_addr(int port, char* mac) { return Status::NOT_SUPPORTED; } +void RivermaxMgr::RivermaxMgrImpl::apply_burst_pool_configuration_to_service(uint32_t service_id) { + // Extract port_id and queue_id from service_id + int port_id = RivermaxBurst::burst_port_id_from_burst_tag(service_id); + int queue_id = RivermaxBurst::burst_queue_id_from_burst_tag(service_id); + + // Find the service and apply configuration from parsed settings + auto it = rx_services_.find(service_id); + if (it != rx_services_.end()) { + auto service = it->second; + auto rx_service = std::dynamic_pointer_cast(service); + if (rx_service) { + // Apply the burst pool configuration using the service's method + rx_service->apply_burst_pool_configuration(); + + HOLOSCAN_LOG_INFO("Applied burst pool configuration to service {} (port={}, queue={})", + service_id, + port_id, + queue_id); + } else { + HOLOSCAN_LOG_ERROR("Failed to cast service to RivermaxManagerRxService for service {}", + service_id); + } + } else { + HOLOSCAN_LOG_ERROR("Failed to find service {}", service_id); + } +} + RivermaxMgr::RivermaxMgr() : pImpl(std::make_unique()) {} RivermaxMgr::~RivermaxMgr() = default; diff --git a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/burst_manager.cpp b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/burst_manager.cpp index d42495660b..b709a1cdef 100644 --- a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/burst_manager.cpp +++ b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/burst_manager.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -394,8 +395,9 @@ RxBurstsManager::RxBurstsManager(bool send_packet_ext_info, int port_id, int que const uint32_t burst_tag = RivermaxBurst::burst_tag_from_port_and_queue_id(port_id, queue_id); gpu_direct_ = (gpu_id_ != INVALID_GPU_ID); + initial_pool_size_ = DEFAULT_NUM_RX_BURSTS; rx_bursts_mempool_ = - std::make_unique(DEFAULT_NUM_RX_BURSTS, *burst_handler_, burst_tag); + std::make_unique(initial_pool_size_, *burst_handler_, burst_tag); if (!rx_bursts_out_queue_) { rx_bursts_out_queue_ = std::make_shared(); @@ -403,7 +405,18 @@ RxBurstsManager::RxBurstsManager(bool send_packet_ext_info, int port_id, int que } if (burst_out_size_ > RivermaxBurst::MAX_PKT_IN_BURST || burst_out_size_ == 0) - burst_out_size_ = RivermaxBurst::MAX_PKT_IN_BURST; + burst_out_size_ = RivermaxBurst::MAX_PKT_IN_BURST; + + // Initialize timing for capacity monitoring + last_capacity_warning_time_ = std::chrono::steady_clock::now(); + last_capacity_critical_time_ = std::chrono::steady_clock::now(); + + HOLOSCAN_LOG_INFO( + "RxBurstsManager initialized: port={}, queue={}, pool_size={}, adaptive_dropping={}", + port_id_, + queue_id_, + initial_pool_size_, + adaptive_dropping_enabled_); } RxBurstsManager::~RxBurstsManager() { @@ -417,5 +430,156 @@ RxBurstsManager::~RxBurstsManager() { rx_bursts_mempool_->enqueue_burst(burst); } } +std::string RxBurstsManager::get_pool_status_string() const { + uint32_t utilization = get_pool_utilization_percent(); + size_t available = rx_bursts_mempool_->available_bursts(); + + std::ostringstream oss; + oss << "Pool Status: " << utilization << "% available (" << available << "/" << initial_pool_size_ + << "), "; + + if (utilization < pool_critical_threshold_percent_) { + oss << "CRITICAL"; + } else if (utilization < pool_low_threshold_percent_) { + oss << "LOW"; + } else if (utilization < pool_recovery_threshold_percent_) { + oss << "RECOVERING"; + } else { + oss << "HEALTHY"; + } + + return oss.str(); +} + +std::string RxBurstsManager::get_burst_drop_statistics() const { + std::ostringstream oss; + oss << "Burst Drop Stats: total=" << total_bursts_dropped_.load() + << ", low_capacity=" << bursts_dropped_low_capacity_.load() + << ", critical_capacity=" << bursts_dropped_critical_capacity_.load() + << ", capacity_warnings=" << pool_capacity_warnings_.load() + << ", critical_events=" << pool_capacity_critical_events_.load(); + return oss.str(); +} + +bool RxBurstsManager::should_drop_burst_due_to_capacity() { + if (!adaptive_dropping_enabled_) { + return false; + } + + // CORE MONITORING: Check memory pool availability + // utilization = percentage of bursts still available in memory pool + // Low utilization = pool running out of free bursts = memory pressure + uint32_t utilization = get_pool_utilization_percent(); + + // Log capacity status periodically + log_pool_capacity_status(utilization); + + // Critical capacity - definitely drop with video-aware logic + if (utilization < pool_critical_threshold_percent_) { + auto now = std::chrono::steady_clock::now(); + auto time_since_last = + std::chrono::duration_cast(now - last_capacity_critical_time_) + .count(); + + if (time_since_last > 1000) { // Log every second + HOLOSCAN_LOG_ERROR( + "CRITICAL: Pool capacity at {}% - dropping new bursts only (port={}, queue={})", + utilization, + port_id_, + queue_id_); + last_capacity_critical_time_ = now; + } + + // In critical mode, use adaptive dropping but be more aggressive + bool should_drop = should_drop_burst_adaptive(utilization); + if (should_drop) { + bursts_dropped_critical_capacity_++; + total_bursts_dropped_++; + pool_capacity_critical_events_++; + } + return should_drop; + } + + // Low capacity - use adaptive dropping policies + if (utilization < pool_low_threshold_percent_) { + auto now = std::chrono::steady_clock::now(); + auto time_since_last = + std::chrono::duration_cast(now - last_capacity_warning_time_) + .count(); + + if (time_since_last > 5000) { // Log every 5 seconds + HOLOSCAN_LOG_WARN("LOW: Pool capacity at {}% - adaptive burst dropping (port={}, queue={})", + utilization, + port_id_, + queue_id_); + last_capacity_warning_time_ = now; + } + + bool should_drop = should_drop_burst_adaptive(utilization); + if (should_drop) { + bursts_dropped_low_capacity_++; + total_bursts_dropped_++; + pool_capacity_warnings_++; + } + return should_drop; + } + + return false; +} + +void RxBurstsManager::log_pool_capacity_status(uint32_t current_utilization) const { + static thread_local auto last_status_log = std::chrono::steady_clock::now(); + auto now = std::chrono::steady_clock::now(); + auto time_since_last = + std::chrono::duration_cast(now - last_status_log).count(); + + // Log detailed status every 30 seconds when adaptive dropping is enabled + if (adaptive_dropping_enabled_ && time_since_last > 30) { + HOLOSCAN_LOG_INFO("Pool Monitor: {} | {} | Policy: CRITICAL_THRESHOLD | Dropping mode: {}", + get_pool_status_string(), + get_burst_drop_statistics(), + in_critical_dropping_mode_ ? "ACTIVE" : "INACTIVE"); + last_status_log = now; + } +} + +bool RxBurstsManager::should_drop_burst_adaptive(uint32_t current_utilization) const { + switch (burst_drop_policy_) { + case BurstDropPolicy::NONE: + return false; + + case BurstDropPolicy::CRITICAL_THRESHOLD: + default: { + // CORE LOGIC: Drop new bursts when critical, stop when recovered + + // Enter critical dropping mode when pool capacity falls below critical threshold + if (current_utilization < pool_critical_threshold_percent_) { + if (!in_critical_dropping_mode_) { + in_critical_dropping_mode_ = true; + HOLOSCAN_LOG_WARN( + "CRITICAL: Pool capacity {}% - entering burst dropping mode (port={}, queue={})", + current_utilization, + port_id_, + queue_id_); + } + return true; // Drop ALL new bursts in critical mode + } + + // Exit critical dropping mode when pool capacity recovers to target threshold + if (in_critical_dropping_mode_ && current_utilization >= pool_recovery_threshold_percent_) { + in_critical_dropping_mode_ = false; + HOLOSCAN_LOG_INFO( + "RECOVERY: Pool capacity {}% - exiting burst dropping mode (port={}, queue={})", + current_utilization, + port_id_, + queue_id_); + return false; + } + + // Stay in current mode (dropping or not dropping) + return in_critical_dropping_mode_; + } + } +} }; // namespace holoscan::advanced_network diff --git a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/burst_manager.h b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/burst_manager.h index 894fb9d40f..950d13144a 100644 --- a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/burst_manager.h +++ b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/burst_manager.h @@ -20,6 +20,9 @@ #include #include +#include +#include +#include #include @@ -311,9 +314,22 @@ class RivermaxBurst::BurstHandler { */ class RxBurstsManager { public: - static constexpr uint32_t DEFAULT_NUM_RX_BURSTS = 64; + static constexpr uint32_t DEFAULT_NUM_RX_BURSTS = 256; static constexpr uint32_t GET_BURST_TIMEOUT_MS = 1000; + // Pool capacity monitoring thresholds (as percentage of total pool size) + // Default pool capacity thresholds (percentages) - can be overridden via configuration + static constexpr uint32_t DEFAULT_POOL_LOW_CAPACITY_THRESHOLD_PERCENT = 25; // Warning level + static constexpr uint32_t DEFAULT_POOL_CRITICAL_CAPACITY_THRESHOLD_PERCENT = + 10; // Start dropping + static constexpr uint32_t DEFAULT_POOL_RECOVERY_THRESHOLD_PERCENT = 50; // Stop dropping + + // Burst dropping policies (simplified) + enum class BurstDropPolicy { + NONE = 0, // No dropping + CRITICAL_THRESHOLD = 1 // Drop new bursts when critical, stop when recovered (default) + }; + /** * @brief Constructor for the RxBurstsManager class. * @@ -400,6 +416,73 @@ class RxBurstsManager { */ void rx_burst_done(RivermaxBurst* burst); + /** + * @brief Gets the current pool capacity utilization as a percentage. + * + * This monitors the MEMORY POOL where we allocate new bursts from. + * Lower percentage = fewer available bursts = higher memory pressure. + * + * @return Pool utilization percentage (0-100). + * 100% = all bursts available, 0% = no bursts available (pool exhausted) + */ + inline uint32_t get_pool_utilization_percent() const { + if (initial_pool_size_ == 0) + return 0; + size_t available = rx_bursts_mempool_->available_bursts(); + return static_cast((available * 100) / initial_pool_size_); + } + + /** + * @brief Checks if pool capacity is below the specified threshold. + * + * @param threshold_percent Threshold percentage (0-100). + * @return True if pool capacity is below threshold. + */ + inline bool is_pool_capacity_below_threshold(uint32_t threshold_percent) const { + return get_pool_utilization_percent() < threshold_percent; + } + + /** + * @brief Gets pool capacity status for monitoring. + * + * @return String description of current pool status. + */ + std::string get_pool_status_string() const; + + /** + * @brief Enables or disables adaptive burst dropping. + * + * @param enabled True to enable adaptive dropping. + * @param policy Burst dropping policy to use. + */ + inline void set_adaptive_burst_dropping( + bool enabled, BurstDropPolicy policy = BurstDropPolicy::CRITICAL_THRESHOLD) { + adaptive_dropping_enabled_ = enabled; + burst_drop_policy_ = policy; + } + + /** + * @brief Configure pool capacity thresholds for adaptive dropping. + * + * @param low_threshold_percent Pool capacity % that triggers low capacity warnings (0-100) + * @param critical_threshold_percent Pool capacity % that triggers burst dropping (0-100) + * @param recovery_threshold_percent Pool capacity % that stops burst dropping (0-100) + */ + inline void configure_pool_thresholds(uint32_t low_threshold_percent, + uint32_t critical_threshold_percent, + uint32_t recovery_threshold_percent) { + pool_low_threshold_percent_ = low_threshold_percent; + pool_critical_threshold_percent_ = critical_threshold_percent; + pool_recovery_threshold_percent_ = recovery_threshold_percent; + } + + /** + * @brief Gets burst dropping statistics. + * + * @return String with burst dropping statistics. + */ + std::string get_burst_drop_statistics() const; + protected: /** * @brief Allocates a new burst. @@ -407,7 +490,30 @@ class RxBurstsManager { * @return Shared pointer to the allocated burst parameters. */ inline std::shared_ptr allocate_burst() { + auto start_time = std::chrono::high_resolution_clock::now(); + auto burst = rx_bursts_mempool_->dequeue_burst(); + + auto end_time = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end_time - start_time); + + if (burst != nullptr) { + HOLOSCAN_LOG_DEBUG( + "allocate_burst: dequeue_burst succeeded in {} μs (port_id: {}, queue_id: {}, burst_id: " + "{})", + duration.count(), + port_id_, + queue_id_, + burst->get_burst_id()); + } else { + HOLOSCAN_LOG_WARN( + "allocate_burst: dequeue_burst FAILED (timeout/no bursts available) in {} μs (port_id: " + "{}, queue_id: {})", + duration.count(), + port_id_, + queue_id_); + } + return burst; } @@ -441,6 +547,15 @@ class RxBurstsManager { return Status::NULL_PTR; } + // Check if we should drop this COMPLETED burst due to critical pool capacity + if (should_drop_burst_due_to_capacity()) { + // Drop the completed burst by returning it to memory pool instead of enqueuing to output + // queue (counter is already incremented inside should_drop_burst_due_to_capacity) + rx_bursts_mempool_->enqueue_burst(cur_out_burst_); + reset_current_burst(); + return Status::SUCCESS; + } + bool res = rx_bursts_out_queue_->enqueue_burst(cur_out_burst_); reset_current_burst(); if (!res) { @@ -456,6 +571,28 @@ class RxBurstsManager { */ inline void reset_current_burst() { cur_out_burst_ = nullptr; } + /** + * @brief Checks pool capacity and decides whether to drop bursts. + * + * @return True if burst should be dropped due to low capacity. + */ + bool should_drop_burst_due_to_capacity(); + + /** + * @brief Logs pool capacity warnings and statistics. + * + * @param current_utilization Current pool utilization percentage. + */ + void log_pool_capacity_status(uint32_t current_utilization) const; + + /** + * @brief Implements generic adaptive burst dropping logic. + * + * @param current_utilization Current pool utilization percentage. + * @return True if burst should be dropped based on network-level policies. + */ + bool should_drop_burst_adaptive(uint32_t current_utilization) const; + protected: bool send_packet_ext_info_ = false; int port_id_ = 0; @@ -472,6 +609,30 @@ class RxBurstsManager { std::shared_ptr cur_out_burst_ = nullptr; AnoBurstExtendedInfo burst_info_; std::unique_ptr burst_handler_; + + // Pool monitoring and adaptive dropping + size_t initial_pool_size_ = DEFAULT_NUM_RX_BURSTS; + bool adaptive_dropping_enabled_ = false; + BurstDropPolicy burst_drop_policy_ = BurstDropPolicy::CRITICAL_THRESHOLD; + + // Configurable thresholds (defaults from constants) + uint32_t pool_low_threshold_percent_ = DEFAULT_POOL_LOW_CAPACITY_THRESHOLD_PERCENT; + uint32_t pool_critical_threshold_percent_ = DEFAULT_POOL_CRITICAL_CAPACITY_THRESHOLD_PERCENT; + uint32_t pool_recovery_threshold_percent_ = DEFAULT_POOL_RECOVERY_THRESHOLD_PERCENT; + + // Critical threshold dropping state + mutable bool in_critical_dropping_mode_ = false; // Track if we're actively dropping + + // Statistics for burst dropping + mutable std::atomic total_bursts_dropped_{0}; + mutable std::atomic bursts_dropped_low_capacity_{0}; + mutable std::atomic bursts_dropped_critical_capacity_{0}; + mutable std::atomic pool_capacity_warnings_{0}; + mutable std::atomic pool_capacity_critical_events_{0}; + + // Performance monitoring + mutable std::chrono::steady_clock::time_point last_capacity_warning_time_; + mutable std::chrono::steady_clock::time_point last_capacity_critical_time_; }; }; // namespace holoscan::advanced_network diff --git a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_config_manager.cpp b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_config_manager.cpp index 2a7964ff64..349327e609 100644 --- a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_config_manager.cpp +++ b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_config_manager.cpp @@ -546,6 +546,72 @@ bool RivermaxConfigParser::parse_common_rx_settings( rivermax_rx_config.stats_report_interval_ms = rx_settings["stats_report_interval_ms"].as(0); + // Parse burst pool adaptive dropping configuration (optional) + const auto& burst_pool_config = rx_settings["burst_pool_adaptive_dropping"]; + if (burst_pool_config) { + rivermax_rx_config.burst_pool_adaptive_dropping_enabled = + burst_pool_config["enabled"].as(false); + rivermax_rx_config.burst_pool_low_threshold_percent = + burst_pool_config["low_threshold_percent"].as(25); + rivermax_rx_config.burst_pool_critical_threshold_percent = + burst_pool_config["critical_threshold_percent"].as(10); + rivermax_rx_config.burst_pool_recovery_threshold_percent = + burst_pool_config["recovery_threshold_percent"].as(50); + + // Validate threshold percentages + uint32_t critical = rivermax_rx_config.burst_pool_critical_threshold_percent; + uint32_t low = rivermax_rx_config.burst_pool_low_threshold_percent; + uint32_t recovery = rivermax_rx_config.burst_pool_recovery_threshold_percent; + + // Check valid range (0..100) + if (critical > 100 || low > 100 || recovery > 100) { + HOLOSCAN_LOG_ERROR( + "Invalid burst pool threshold percentages: all values must be in range 0..100 " + "(critical={}, low={}, recovery={})", + critical, low, recovery); + return false; + } + + // Check for nonsensical zero values + if (critical == 0 || recovery == 0) { + HOLOSCAN_LOG_ERROR( + "Invalid burst pool threshold percentages: critical and recovery cannot be 0 " + "(critical={}, low={}, recovery={})", + critical, low, recovery); + return false; + } + + // Check proper ordering: critical < low < recovery + if (critical >= low) { + HOLOSCAN_LOG_ERROR( + "Invalid burst pool threshold ordering: critical must be < low " + "(critical={}, low={})", + critical, low); + return false; + } + + if (low >= recovery) { + HOLOSCAN_LOG_ERROR( + "Invalid burst pool threshold ordering: low must be < recovery " + "(low={}, recovery={})", + low, recovery); + return false; + } + + HOLOSCAN_LOG_INFO( + "Parsed burst pool adaptive dropping config: enabled={}, thresholds={}%/{}%/{}%", + rivermax_rx_config.burst_pool_adaptive_dropping_enabled, + rivermax_rx_config.burst_pool_low_threshold_percent, + rivermax_rx_config.burst_pool_critical_threshold_percent, + rivermax_rx_config.burst_pool_recovery_threshold_percent); + } else { + // Use default values if not specified + rivermax_rx_config.burst_pool_adaptive_dropping_enabled = false; + rivermax_rx_config.burst_pool_low_threshold_percent = 25; + rivermax_rx_config.burst_pool_critical_threshold_percent = 10; + rivermax_rx_config.burst_pool_recovery_threshold_percent = 50; + } + return true; } diff --git a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_mgr_service.cpp b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_mgr_service.cpp index ed25652c4d..2b2f5c92eb 100644 --- a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_mgr_service.cpp +++ b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_mgr_service.cpp @@ -71,6 +71,24 @@ bool RivermaxManagerRxService::initialize() { return true; } +void RivermaxManagerRxService::apply_burst_pool_configuration() { + if (rx_burst_manager_) { + // Apply the configuration to the burst manager + rx_burst_manager_->set_adaptive_burst_dropping(burst_pool_adaptive_dropping_enabled_); + rx_burst_manager_->configure_pool_thresholds(burst_pool_low_threshold_percent_, + burst_pool_critical_threshold_percent_, + burst_pool_recovery_threshold_percent_); + + HOLOSCAN_LOG_INFO("Applied burst pool configuration: enabled={}, thresholds={}%/{}%/{}%", + burst_pool_adaptive_dropping_enabled_, + burst_pool_low_threshold_percent_, + burst_pool_critical_threshold_percent_, + burst_pool_recovery_threshold_percent_); + } else { + HOLOSCAN_LOG_ERROR("Cannot apply burst pool configuration: burst manager not initialized"); + } +} + void RivermaxManagerRxService::free_rx_burst(BurstParams* burst) { if (!rx_burst_manager_) { HOLOSCAN_LOG_ERROR("RX burst manager not initialized"); @@ -125,6 +143,15 @@ bool IPOReceiverService::configure_service() { send_packet_ext_info_ = ipo_receiver_builder_->send_packet_ext_info_; gpu_id_ = ipo_receiver_builder_->built_settings_.gpu_id; max_chunk_size_ = ipo_receiver_builder_->built_settings_.max_packets_in_rx_chunk; + + // Copy burst pool configuration + burst_pool_adaptive_dropping_enabled_ = + ipo_receiver_builder_->burst_pool_adaptive_dropping_enabled_; + burst_pool_low_threshold_percent_ = ipo_receiver_builder_->burst_pool_low_threshold_percent_; + burst_pool_critical_threshold_percent_ = + ipo_receiver_builder_->burst_pool_critical_threshold_percent_; + burst_pool_recovery_threshold_percent_ = + ipo_receiver_builder_->burst_pool_recovery_threshold_percent_; return true; } @@ -194,6 +221,15 @@ bool RTPReceiverService::configure_service() { send_packet_ext_info_ = rtp_receiver_builder_->send_packet_ext_info_; gpu_id_ = rtp_receiver_builder_->built_settings_.gpu_id; max_chunk_size_ = rtp_receiver_builder_->max_chunk_size_; + + // Copy burst pool configuration + burst_pool_adaptive_dropping_enabled_ = + rtp_receiver_builder_->burst_pool_adaptive_dropping_enabled_; + burst_pool_low_threshold_percent_ = rtp_receiver_builder_->burst_pool_low_threshold_percent_; + burst_pool_critical_threshold_percent_ = + rtp_receiver_builder_->burst_pool_critical_threshold_percent_; + burst_pool_recovery_threshold_percent_ = + rtp_receiver_builder_->burst_pool_recovery_threshold_percent_; return true; } @@ -540,7 +576,13 @@ void MediaSenderService::free_tx_burst(BurstParams* burst) { } void MediaSenderService::shutdown() { - if (processing_frame_) { processing_frame_.reset(); } + { + std::lock_guard lock(mutex_); + if (processing_frame_) { + processing_frame_.reset(); + } + } + if (tx_media_frame_provider_) { tx_media_frame_provider_->stop(); } if (tx_media_frame_pool_) { tx_media_frame_pool_->stop(); } } diff --git a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_mgr_service.h b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_mgr_service.h index 063fef8027..3902922c0e 100644 --- a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_mgr_service.h +++ b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_mgr_service.h @@ -20,6 +20,7 @@ #include #include +#include #include @@ -141,6 +142,21 @@ class RivermaxManagerRxService : public RivermaxManagerService { */ virtual void print_stats(std::stringstream& ss) const {} + /** + * @brief Gets the burst manager for this service. + * + * @return Shared pointer to the burst manager. + */ + std::shared_ptr get_burst_manager() const { return rx_burst_manager_; } + + /** + * @brief Applies the parsed burst pool configuration to the burst manager. + * + * This method configures the burst manager with the burst pool adaptive dropping + * settings that were parsed from the YAML configuration. + */ + void apply_burst_pool_configuration(); + bool initialize() override; void run() override; void shutdown() override; @@ -174,6 +190,12 @@ class RivermaxManagerRxService : public RivermaxManagerService { bool send_packet_ext_info_ = false; ///< Flag for extended packet info int gpu_id_ = INVALID_GPU_ID; ///< GPU device ID size_t max_chunk_size_ = 0; ///< Maximum chunk size for received data + + // Burst pool adaptive dropping configuration (private) + bool burst_pool_adaptive_dropping_enabled_ = false; + uint32_t burst_pool_low_threshold_percent_ = 25; + uint32_t burst_pool_critical_threshold_percent_ = 10; + uint32_t burst_pool_recovery_threshold_percent_ = 50; }; /** diff --git a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_queue_configs.cpp b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_queue_configs.cpp index 700e1f9e9e..49bb1b4123 100644 --- a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_queue_configs.cpp +++ b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_queue_configs.cpp @@ -45,7 +45,11 @@ RivermaxCommonRxQueueConfig::RivermaxCommonRxQueueConfig(const RivermaxCommonRxQ send_packet_ext_info(other.send_packet_ext_info), stats_report_interval_ms(other.stats_report_interval_ms), cpu_cores(other.cpu_cores), - master_core(other.master_core) {} + master_core(other.master_core), + burst_pool_adaptive_dropping_enabled(other.burst_pool_adaptive_dropping_enabled), + burst_pool_low_threshold_percent(other.burst_pool_low_threshold_percent), + burst_pool_critical_threshold_percent(other.burst_pool_critical_threshold_percent), + burst_pool_recovery_threshold_percent(other.burst_pool_recovery_threshold_percent) {} RivermaxCommonRxQueueConfig& RivermaxCommonRxQueueConfig::operator=( const RivermaxCommonRxQueueConfig& other) { @@ -68,6 +72,10 @@ RivermaxCommonRxQueueConfig& RivermaxCommonRxQueueConfig::operator=( stats_report_interval_ms = other.stats_report_interval_ms; cpu_cores = other.cpu_cores; master_core = other.master_core; + burst_pool_adaptive_dropping_enabled = other.burst_pool_adaptive_dropping_enabled; + burst_pool_low_threshold_percent = other.burst_pool_low_threshold_percent; + burst_pool_critical_threshold_percent = other.burst_pool_critical_threshold_percent; + burst_pool_recovery_threshold_percent = other.burst_pool_recovery_threshold_percent; return *this; } @@ -432,6 +440,13 @@ ReturnStatus RivermaxQueueToIPOReceiverSettingsBuilder::convert_settings( target_settings->max_packets_in_rx_chunk = source_settings->max_chunk_size; send_packet_ext_info_ = source_settings->send_packet_ext_info; + + // Copy burst pool configuration + burst_pool_adaptive_dropping_enabled_ = source_settings->burst_pool_adaptive_dropping_enabled; + burst_pool_low_threshold_percent_ = source_settings->burst_pool_low_threshold_percent; + burst_pool_critical_threshold_percent_ = source_settings->burst_pool_critical_threshold_percent; + burst_pool_recovery_threshold_percent_ = source_settings->burst_pool_recovery_threshold_percent; + settings_built_ = true; built_settings_ = *target_settings; @@ -483,6 +498,12 @@ ReturnStatus RivermaxQueueToRTPReceiverSettingsBuilder::convert_settings( max_chunk_size_ = source_settings->max_chunk_size; send_packet_ext_info_ = source_settings->send_packet_ext_info; + // Copy burst pool configuration + burst_pool_adaptive_dropping_enabled_ = source_settings->burst_pool_adaptive_dropping_enabled; + burst_pool_low_threshold_percent_ = source_settings->burst_pool_low_threshold_percent; + burst_pool_critical_threshold_percent_ = source_settings->burst_pool_critical_threshold_percent; + burst_pool_recovery_threshold_percent_ = source_settings->burst_pool_recovery_threshold_percent; + settings_built_ = true; built_settings_ = *target_settings; diff --git a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_queue_configs.h b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_queue_configs.h index 3164cad2be..1b1eecef8b 100644 --- a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_queue_configs.h +++ b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_queue_configs.h @@ -106,6 +106,12 @@ struct RivermaxCommonRxQueueConfig : public BaseQueueConfig { uint32_t stats_report_interval_ms; std::string cpu_cores; int master_core; + + // Burst pool adaptive dropping configuration + bool burst_pool_adaptive_dropping_enabled = false; + uint32_t burst_pool_low_threshold_percent = 25; + uint32_t burst_pool_critical_threshold_percent = 10; + uint32_t burst_pool_recovery_threshold_percent = 50; }; /** @@ -264,6 +270,13 @@ class RivermaxQueueToIPOReceiverSettingsBuilder public: static constexpr int USECS_IN_SECOND = 1000000; bool send_packet_ext_info_ = false; + + // Burst pool adaptive dropping configuration + bool burst_pool_adaptive_dropping_enabled_ = false; + uint32_t burst_pool_low_threshold_percent_ = 25; + uint32_t burst_pool_critical_threshold_percent_ = 10; + uint32_t burst_pool_recovery_threshold_percent_ = 50; + IPOReceiverSettings built_settings_; bool settings_built_ = false; }; @@ -286,6 +299,13 @@ class RivermaxQueueToRTPReceiverSettingsBuilder static constexpr int USECS_IN_SECOND = 1000000; bool send_packet_ext_info_ = false; size_t max_chunk_size_ = 0; + + // Burst pool adaptive dropping configuration + bool burst_pool_adaptive_dropping_enabled_ = false; + uint32_t burst_pool_low_threshold_percent_ = 25; + uint32_t burst_pool_critical_threshold_percent_ = 10; + uint32_t burst_pool_recovery_threshold_percent_ = 50; + RTPReceiverSettings built_settings_; bool settings_built_ = false; }; From 8726cd733dc1bc4013822250aabd86df31352380 Mon Sep 17 00:00:00 2001 From: Rony Rado Date: Wed, 29 Oct 2025 15:48:37 +0000 Subject: [PATCH 5/6] feat(advanced_network) Lint fixes Lint fixes Signed-off-by: Rony Rado --- .../adv_network_rivermax_mgr.cpp | 27 +++-- .../rivermax_mgr_impl/burst_manager.cpp | 42 ++++--- .../rivermax_mgr_impl/burst_manager.h | 16 ++- .../rivermax_config_manager.cpp | 45 +++++--- .../rivermax_mgr_service.cpp | 64 +++++++---- .../rivermax_mgr_impl/rivermax_mgr_service.h | 2 +- .../rivermax_queue_configs.cpp | 104 +++++++++++++----- 7 files changed, 205 insertions(+), 95 deletions(-) diff --git a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/adv_network_rivermax_mgr.cpp b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/adv_network_rivermax_mgr.cpp index fbf04abb5c..14b542d6ac 100644 --- a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/adv_network_rivermax_mgr.cpp +++ b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/adv_network_rivermax_mgr.cpp @@ -368,7 +368,9 @@ RivermaxMgr::RivermaxMgrImpl::~RivermaxMgrImpl() {} void RivermaxMgr::RivermaxMgrImpl::run() { std::size_t num_services = rx_services_.size(); - if (num_services > 0) { HOLOSCAN_LOG_INFO("Starting {} RX Services", num_services); } + if (num_services > 0) { + HOLOSCAN_LOG_INFO("Starting {} RX Services", num_services); + } for (const auto& entry : rx_services_) { uint32_t key = entry.first; @@ -378,7 +380,9 @@ void RivermaxMgr::RivermaxMgrImpl::run() { } num_services = tx_services_.size(); - if (num_services > 0) { HOLOSCAN_LOG_INFO("Starting {} TX Services", num_services); } + if (num_services > 0) { + HOLOSCAN_LOG_INFO("Starting {} TX Services", num_services); + } for (const auto& entry : tx_services_) { uint32_t key = entry.first; @@ -413,7 +417,8 @@ uint16_t RivermaxMgr::RivermaxMgrImpl::get_packet_length(BurstParams* burst, int void* RivermaxMgr::RivermaxMgrImpl::get_packet_extra_info(BurstParams* burst, int idx) { RivermaxBurst* rivermax_burst = static_cast(burst); - if (rivermax_burst->is_packet_info_per_packet()) return burst->pkt_extra_info[idx]; + if (rivermax_burst->is_packet_info_per_packet()) + return burst->pkt_extra_info[idx]; return nullptr; } @@ -524,7 +529,9 @@ Status RivermaxMgr::RivermaxMgrImpl::get_rx_burst(BurstParams** burst, int port, } auto out_burst_shared = queue_it->second->dequeue_burst(); - if (out_burst_shared == nullptr) { return Status::NULL_PTR; } + if (out_burst_shared == nullptr) { + return Status::NULL_PTR; + } *burst = out_burst_shared.get(); return Status::SUCCESS; } @@ -552,7 +559,9 @@ Status RivermaxMgr::RivermaxMgrImpl::send_tx_burst(BurstParams* burst) { } void RivermaxMgr::RivermaxMgrImpl::shutdown() { - if (force_quit.load()) { return; } + if (force_quit.load()) { + return; + } HOLOSCAN_LOG_INFO("Advanced Network Rivermax manager shutting down"); force_quit.store(true); print_stats(); @@ -569,10 +578,14 @@ void RivermaxMgr::RivermaxMgrImpl::shutdown() { } for (auto& rx_service_thread : rx_service_threads_) { - if (rx_service_thread.joinable()) { rx_service_thread.join(); } + if (rx_service_thread.joinable()) { + rx_service_thread.join(); + } } for (auto& tx_service_thread : tx_service_threads_) { - if (tx_service_thread.joinable()) { tx_service_thread.join(); } + if (tx_service_thread.joinable()) { + tx_service_thread.join(); + } } HOLOSCAN_LOG_INFO("All service threads finished"); rx_services_.clear(); diff --git a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/burst_manager.cpp b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/burst_manager.cpp index b709a1cdef..9e349e429b 100644 --- a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/burst_manager.cpp +++ b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/burst_manager.cpp @@ -48,14 +48,18 @@ class NonBlockingQueue : public QueueInterface { public: void enqueue(const T& value) override { - if (stop_) { return; } + if (stop_) { + return; + } std::lock_guard lock(mutex_); queue_.push(value); } bool try_dequeue(T& value) override { std::lock_guard lock(mutex_); - if (queue_.empty() || stop_) { return false; } + if (queue_.empty() || stop_) { + return false; + } value = queue_.front(); queue_.pop(); return true; @@ -75,9 +79,7 @@ class NonBlockingQueue : public QueueInterface { while (!queue_.empty()) { queue_.pop(); } } - void stop() override { - stop_ = true; - } + void stop() override { stop_ = true; } }; /** @@ -94,7 +96,9 @@ class BlockingQueue : public QueueInterface { public: void enqueue(const T& value) override { - if (stop_) { return; } + if (stop_) { + return; + } std::lock_guard lock(mutex_); queue_.push(value); cond_.notify_one(); @@ -103,7 +107,9 @@ class BlockingQueue : public QueueInterface { bool try_dequeue(T& value) override { std::unique_lock lock(mutex_); cond_.wait(lock, [this] { return !queue_.empty() || stop_; }); - if (stop_) { return false; } + if (stop_) { + return false; + } value = queue_.front(); queue_.pop(); return true; @@ -112,9 +118,11 @@ class BlockingQueue : public QueueInterface { bool try_dequeue(T& value, std::chrono::milliseconds timeout) override { std::unique_lock lock(mutex_); if (!cond_.wait_for(lock, timeout, [this] { return !queue_.empty() || stop_; })) { - return false; + return false; + } + if (stop_) { + return false; } - if (stop_) { return false; } value = queue_.front(); queue_.pop(); return true; @@ -131,8 +139,8 @@ class BlockingQueue : public QueueInterface { } void stop() override { - stop_ = true; - cond_.notify_all(); + stop_ = true; + cond_.notify_all(); } }; @@ -237,7 +245,7 @@ std::shared_ptr AnoBurstsMemoryPool::dequeue_burst() { std::shared_ptr burst; if (queue_->try_dequeue(burst, - std::chrono::milliseconds(RxBurstsManager::GET_BURST_TIMEOUT_MS))) { + std::chrono::milliseconds(RxBurstsManager::GET_BURST_TIMEOUT_MS))) { return burst; } return nullptr; @@ -278,7 +286,7 @@ std::shared_ptr AnoBurstsQueue::dequeue_burst() { std::shared_ptr burst; if (queue_->try_dequeue(burst, - std::chrono::milliseconds(RxBurstsManager::GET_BURST_TIMEOUT_MS))) { + std::chrono::milliseconds(RxBurstsManager::GET_BURST_TIMEOUT_MS))) { return burst; } return nullptr; @@ -420,16 +428,20 @@ RxBurstsManager::RxBurstsManager(bool send_packet_ext_info, int port_id, int que } RxBurstsManager::~RxBurstsManager() { - if (using_shared_out_queue_) { return; } + if (using_shared_out_queue_) { + return; + } std::shared_ptr burst; // Get all bursts from the queue and return them to the memory pool while (rx_bursts_out_queue_->available_bursts() > 0) { burst = rx_bursts_out_queue_->dequeue_burst(); - if (burst == nullptr) break; + if (burst == nullptr) + break; rx_bursts_mempool_->enqueue_burst(burst); } } + std::string RxBurstsManager::get_pool_status_string() const { uint32_t utilization = get_pool_utilization_percent(); size_t available = rx_bursts_mempool_->available_bursts(); diff --git a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/burst_manager.h b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/burst_manager.h index 950d13144a..3647dffffc 100644 --- a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/burst_manager.h +++ b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/burst_manager.h @@ -87,7 +87,9 @@ class RivermaxBurst : public BurstParams { */ inline uint16_t get_burst_id() const { auto burst_info = get_burst_info(); - if (burst_info == nullptr) { return 0; } + if (burst_info == nullptr) { + return 0; + } return burst_info->burst_id; } @@ -140,7 +142,9 @@ class RivermaxBurst : public BurstParams { auto burst_info = get_burst_info(); - if (burst_info == nullptr) { return; } + if (burst_info == nullptr) { + return; + } burst_info->hds_on = hds_on; burst_info->header_stride_size = header_stride_size; @@ -164,9 +168,7 @@ class RivermaxBurst : public BurstParams { * * @return The flags of the burst. */ - inline BurstFlags get_burst_flags() const { - return static_cast(hdr.hdr.burst_flags); - } + inline BurstFlags get_burst_flags() const { return static_cast(hdr.hdr.burst_flags); } /** * @brief Gets the extended info of a burst. @@ -405,7 +407,9 @@ class RxBurstsManager { auto out_burst = rx_bursts_out_queue_->dequeue_burst().get(); *burst = static_cast(out_burst); - if (*burst == nullptr) { return Status::NULL_PTR; } + if (*burst == nullptr) { + return Status::NULL_PTR; + } return Status::SUCCESS; } diff --git a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_config_manager.cpp b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_config_manager.cpp index 349327e609..4a62ecddae 100644 --- a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_config_manager.cpp +++ b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_config_manager.cpp @@ -89,8 +89,9 @@ bool RivermaxConfigContainer::parse_configuration(const NetworkConfig& cfg) { set_rivermax_log_level(cfg.log_level_); if (rivermax_rx_config_found == 0 && rivermax_tx_config_found == 0) { - HOLOSCAN_LOG_ERROR("Failed to parse Rivermax advanced_network settings. " - "No valid settings found"); + HOLOSCAN_LOG_ERROR( + "Failed to parse Rivermax advanced_network settings. " + "No valid settings found"); return false; } @@ -112,12 +113,16 @@ int RivermaxConfigContainer::parse_rx_queues(uint16_t port_id, auto rx_config_manager = std::dynamic_pointer_cast( get_config_manager(RivermaxConfigContainer::ConfigType::RX)); - if (!rx_config_manager) { return 0; } + if (!rx_config_manager) { + return 0; + } rx_config_manager->set_configuration(cfg_); for (const auto& q : queues) { - if (!rx_config_manager->append_candidate_for_rx_queue(port_id, q)) { continue; } + if (!rx_config_manager->append_candidate_for_rx_queue(port_id, q)) { + continue; + } rivermax_rx_config_found++; } @@ -208,7 +213,9 @@ bool RxConfigManager::append_ipo_receiver_candidate_for_rx_queue( return false; } - if (config_memory_allocator(rivermax_rx_config, q) == false) { return false; } + if (config_memory_allocator(rivermax_rx_config, q) == false) { + return false; + } rivermax_rx_config.cpu_cores = q.common_.cpu_core_; rivermax_rx_config.master_core = cfg_.common_.master_core_; @@ -239,7 +246,9 @@ bool RxConfigManager::append_rtp_receiver_candidate_for_rx_queue( return false; } - if (config_memory_allocator(rivermax_rx_config, q) == false) { return false; } + if (config_memory_allocator(rivermax_rx_config, q) == false) { + return false; + } rivermax_rx_config.cpu_cores = q.common_.cpu_core_; rivermax_rx_config.master_core = cfg_.common_.master_core_; @@ -270,12 +279,16 @@ int RivermaxConfigContainer::parse_tx_queues(uint16_t port_id, auto tx_config_manager = std::dynamic_pointer_cast( get_config_manager(RivermaxConfigContainer::ConfigType::TX)); - if (!tx_config_manager) { return 0; } + if (!tx_config_manager) { + return 0; + } tx_config_manager->set_configuration(cfg_); for (const auto& q : queues) { - if (!tx_config_manager->append_candidate_for_tx_queue(port_id, q)) { continue; } + if (!tx_config_manager->append_candidate_for_tx_queue(port_id, q)) { + continue; + } rivermax_tx_config_found++; } @@ -355,7 +368,9 @@ bool TxConfigManager::append_media_sender_candidate_for_tx_queue( return false; } - if (config_memory_allocator(rivermax_tx_config, q) == false) { return false; } + if (config_memory_allocator(rivermax_tx_config, q) == false) { + return false; + } rivermax_tx_config.cpu_cores = q.common_.cpu_core_; rivermax_tx_config.master_core = cfg_.common_.master_core_; @@ -697,8 +712,7 @@ bool RivermaxConfigParser::parse_common_tx_settings( rivermax_tx_config.print_parameters = tx_settings["verbose"].as(false); rivermax_tx_config.num_of_threads = tx_settings["num_of_threads"].as(1); rivermax_tx_config.send_packet_ext_info = tx_settings["send_packet_ext_info"].as(true); - rivermax_tx_config.num_of_packets_in_chunk = - tx_settings["num_of_packets_in_chunk"].as( + rivermax_tx_config.num_of_packets_in_chunk = tx_settings["num_of_packets_in_chunk"].as( MediaSenderSettings::DEFAULT_NUM_OF_PACKETS_IN_CHUNK_FHD); rivermax_tx_config.sleep_between_operations = tx_settings["sleep_between_operations"].as(true); @@ -719,9 +733,8 @@ bool RivermaxConfigParser::parse_media_sender_settings( rivermax_tx_config.use_internal_memory_pool = tx_settings["use_internal_memory_pool"].as(false); if (rivermax_tx_config.use_internal_memory_pool) { - rivermax_tx_config.memory_pool_location = - GetMemoryKindFromString(tx_settings["memory_pool_location"].template - as("device")); + rivermax_tx_config.memory_pool_location = GetMemoryKindFromString( + tx_settings["memory_pool_location"].template as("device")); if (rivermax_tx_config.memory_pool_location == MemoryKind::INVALID) { rivermax_tx_config.memory_pool_location = MemoryKind::DEVICE; HOLOSCAN_LOG_ERROR("Invalid memory pool location, setting to DEVICE"); @@ -826,7 +839,9 @@ bool ConfigManagerUtilities::validate_cores(const std::string& cores) { void ConfigManagerUtilities::set_allocator_type(AppSettings& app_settings_config, const std::string& allocator_type) { auto setAllocatorType = [&](const std::string& allocatorTypeStr, AllocatorTypeUI allocatorType) { - if (allocator_type == allocatorTypeStr) { app_settings_config.allocator_type = allocatorType; } + if (allocator_type == allocatorTypeStr) { + app_settings_config.allocator_type = allocatorType; + } }; app_settings_config.allocator_type = AllocatorTypeUI::Auto; diff --git a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_mgr_service.cpp b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_mgr_service.cpp index 2b2f5c92eb..2678b24dcd 100644 --- a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_mgr_service.cpp +++ b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_mgr_service.cpp @@ -58,8 +58,8 @@ bool RivermaxManagerRxService::initialize() { rx_packet_processor_ = std::make_shared(rx_burst_manager_); - auto rivermax_chunk_consumer = std::make_unique(rx_packet_processor_, - max_chunk_size_); + auto rivermax_chunk_consumer = + std::make_unique(rx_packet_processor_, max_chunk_size_); auto status = rx_service_->set_receive_data_consumer(0, std::move(rivermax_chunk_consumer)); if (status != ReturnStatus::success) { @@ -113,7 +113,9 @@ void RivermaxManagerRxService::run() { } void RivermaxManagerRxService::shutdown() { - if (rx_service_) { HOLOSCAN_LOG_INFO("Shutting down Receiver:{}", service_id_); } + if (rx_service_) { + HOLOSCAN_LOG_INFO("Shutting down Receiver:{}", service_id_); + } initialized_ = false; } @@ -124,7 +126,9 @@ Status RivermaxManagerRxService::get_rx_burst(BurstParams** burst) { } auto out_burst = rx_bursts_out_queue_->dequeue_burst().get(); *burst = static_cast(out_burst); - if (*burst == nullptr) { return Status::NOT_READY; } + if (*burst == nullptr) { + return Status::NOT_READY; + } return Status::SUCCESS; } @@ -182,7 +186,9 @@ void IPOReceiverService::print_stats(std::stringstream& ss) const { ss << " dropped: "; for (uint32_t s_index = 0; s_index < stream_stats[i].path_stats.size(); ++s_index) { - if (s_index > 0) { ss << ", "; } + if (s_index > 0) { + ss << ", "; + } ss << stream_stats[i].path_stats[s_index].rx_dropped + stream_stats[i].rx_dropped; } ss << " |" @@ -318,7 +324,9 @@ void RivermaxManagerTxService::run() { } void RivermaxManagerTxService::shutdown() { - if (tx_service_) { HOLOSCAN_LOG_INFO("Shutting down TX Service:{}", service_id_); } + if (tx_service_) { + HOLOSCAN_LOG_INFO("Shutting down TX Service:{}", service_id_); + } initialized_ = false; } @@ -561,7 +569,9 @@ Status MediaSenderService::send_tx_burst(BurstParams* burst) { } bool MediaSenderService::is_tx_burst_available(BurstParams* burst) { - if (!initialized_ || !tx_media_frame_pool_) { return false; } + if (!initialized_ || !tx_media_frame_pool_) { + return false; + } // Check if we have available frames in the pool and no current processing frame return (tx_media_frame_pool_->get_available_frames_count() > 0 && !processing_frame_); } @@ -572,7 +582,9 @@ void MediaSenderService::free_tx_burst(BurstParams* burst) { std::lock_guard lock(mutex_); HOLOSCAN_LOG_TRACE( "MediaSenderService{}:{}::free_tx_burst(): Processing frame was reset", port_id_, queue_id_); - if (processing_frame_) { processing_frame_.reset(); } + if (processing_frame_) { + processing_frame_.reset(); + } } void MediaSenderService::shutdown() { @@ -583,8 +595,12 @@ void MediaSenderService::shutdown() { } } - if (tx_media_frame_provider_) { tx_media_frame_provider_->stop(); } - if (tx_media_frame_pool_) { tx_media_frame_pool_->stop(); } + if (tx_media_frame_provider_) { + tx_media_frame_provider_->stop(); + } + if (tx_media_frame_pool_) { + tx_media_frame_pool_->stop(); + } } MediaSenderZeroCopyService::MediaSenderZeroCopyService( @@ -618,10 +634,11 @@ Status MediaSenderZeroCopyService::get_tx_packet_burst(BurstParams* burst) { return Status::INVALID_PARAMETER; } if (burst->hdr.hdr.q_id != queue_id_ || burst->hdr.hdr.port_id != port_id_) { - HOLOSCAN_LOG_ERROR("MediaSenderZeroCopyService{}:{}::get_tx_packet_burst(): Burst queue ID " - "mismatch", - port_id_, - queue_id_); + HOLOSCAN_LOG_ERROR( + "MediaSenderZeroCopyService{}:{}::get_tx_packet_burst(): Burst queue ID " + "mismatch", + port_id_, + queue_id_); return Status::INVALID_PARAMETER; } @@ -653,7 +670,7 @@ Status MediaSenderZeroCopyService::send_tx_burst(BurstParams* burst) { return Status::INVALID_PARAMETER; } std::shared_ptr out_frame = - std::static_pointer_cast(burst->custom_pkt_data); + std::static_pointer_cast(burst->custom_pkt_data); burst->custom_pkt_data.reset(); if (!out_frame) { HOLOSCAN_LOG_ERROR( @@ -684,24 +701,27 @@ Status MediaSenderZeroCopyService::send_tx_burst(BurstParams* burst) { } bool MediaSenderZeroCopyService::is_tx_burst_available(BurstParams* burst) { - if (!initialized_) { return false; } + if (!initialized_) { + return false; + } return (!is_frame_in_process_ && - tx_media_frame_provider_->get_queue_size() < MEDIA_FRAME_PROVIDER_SIZE); + tx_media_frame_provider_->get_queue_size() < MEDIA_FRAME_PROVIDER_SIZE); } void MediaSenderZeroCopyService::free_tx_burst(BurstParams* burst) { // If we have a processing frame but we're told to free the burst, // we should clear the processing frame flag std::lock_guard lock(mutex_); - HOLOSCAN_LOG_TRACE( - "MediaSenderZeroCopyService{}:{}::free_tx_burst(): Processing frame was reset", - port_id_, - queue_id_); + HOLOSCAN_LOG_TRACE("MediaSenderZeroCopyService{}:{}::free_tx_burst(): Processing frame was reset", + port_id_, + queue_id_); is_frame_in_process_ = false; } void MediaSenderZeroCopyService::shutdown() { - if (tx_media_frame_provider_) { tx_media_frame_provider_->stop(); } + if (tx_media_frame_provider_) { + tx_media_frame_provider_->stop(); + } } } // namespace holoscan::advanced_network diff --git a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_mgr_service.h b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_mgr_service.h index 3902922c0e..78a52c599f 100644 --- a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_mgr_service.h +++ b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_mgr_service.h @@ -529,7 +529,7 @@ class MediaSenderZeroCopyService : public MediaSenderBaseService { private: std::shared_ptr - tx_media_frame_provider_; ///< Provider for buffered media frames + tx_media_frame_provider_; ///< Provider for buffered media frames bool is_frame_in_process_ = false; mutable std::mutex mutex_; diff --git a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_queue_configs.cpp b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_queue_configs.cpp index 49bb1b4123..92a9e8b053 100644 --- a/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_queue_configs.cpp +++ b/operators/advanced_network/advanced_network/managers/rivermax/rivermax_mgr_impl/rivermax_queue_configs.cpp @@ -53,7 +53,9 @@ RivermaxCommonRxQueueConfig::RivermaxCommonRxQueueConfig(const RivermaxCommonRxQ RivermaxCommonRxQueueConfig& RivermaxCommonRxQueueConfig::operator=( const RivermaxCommonRxQueueConfig& other) { - if (this == &other) { return *this; } + if (this == &other) { + return *this; + } BaseQueueConfig::operator=(other); max_packet_size = other.max_packet_size; max_chunk_size = other.max_chunk_size; @@ -90,7 +92,9 @@ RivermaxIPOReceiverQueueConfig::RivermaxIPOReceiverQueueConfig( RivermaxIPOReceiverQueueConfig& RivermaxIPOReceiverQueueConfig::operator=( const RivermaxIPOReceiverQueueConfig& other) { - if (this == &other) { return *this; } + if (this == &other) { + return *this; + } RivermaxCommonRxQueueConfig::operator=(other); local_ips = other.local_ips; source_ips = other.source_ips; @@ -110,7 +114,9 @@ RivermaxRTPReceiverQueueConfig::RivermaxRTPReceiverQueueConfig( RivermaxRTPReceiverQueueConfig& RivermaxRTPReceiverQueueConfig::operator=( const RivermaxRTPReceiverQueueConfig& other) { - if (this == &other) { return *this; } + if (this == &other) { + return *this; + } RivermaxCommonRxQueueConfig::operator=(other); local_ip = other.local_ip; source_ip = other.source_ip; @@ -142,7 +148,9 @@ RivermaxCommonTxQueueConfig::RivermaxCommonTxQueueConfig(const RivermaxCommonTxQ RivermaxCommonTxQueueConfig& RivermaxCommonTxQueueConfig::operator=( const RivermaxCommonTxQueueConfig& other) { - if (this == &other) { return *this; } + if (this == &other) { + return *this; + } gpu_direct = other.gpu_direct; gpu_device_id = other.gpu_device_id; lock_gpu_clocks = other.lock_gpu_clocks; @@ -178,7 +186,9 @@ RivermaxMediaSenderQueueConfig::RivermaxMediaSenderQueueConfig( RivermaxMediaSenderQueueConfig& RivermaxMediaSenderQueueConfig::operator=( const RivermaxMediaSenderQueueConfig& other) { - if (this == &other) { return *this; } + if (this == &other) { + return *this; + } RivermaxCommonTxQueueConfig::operator=(other); video_format = other.video_format; bit_depth = other.bit_depth; @@ -289,9 +299,13 @@ void RivermaxMediaSenderQueueConfig::dump_parameters() const { ReturnStatus RivermaxCommonRxQueueValidator::validate( const std::shared_ptr& settings) const { ReturnStatus rc = ValidatorUtils::validate_core(settings->master_core); - if (rc != ReturnStatus::success) { return rc; } + if (rc != ReturnStatus::success) { + return rc; + } bool res = ConfigManagerUtilities::validate_cores(settings->cpu_cores); - if (!res) { return ReturnStatus::failure; } + if (!res) { + return ReturnStatus::failure; + } return ReturnStatus::success; } @@ -300,7 +314,9 @@ ReturnStatus RivermaxIPOReceiverQueueValidator::validate( const std::shared_ptr& settings) const { auto validator = std::make_shared(); ReturnStatus rc = validator->validate(settings); - if (rc != ReturnStatus::success) { return rc; } + if (rc != ReturnStatus::success) { + return rc; + } if (settings->source_ips.empty()) { HOLOSCAN_LOG_ERROR("Source IP addresses are not set for RTP stream"); @@ -325,13 +341,21 @@ ReturnStatus RivermaxIPOReceiverQueueValidator::validate( } rc = ValidatorUtils::validate_ip4_address(settings->source_ips); - if (rc != ReturnStatus::success) { return rc; } + if (rc != ReturnStatus::success) { + return rc; + } rc = ValidatorUtils::validate_ip4_address(settings->local_ips); - if (rc != ReturnStatus::success) { return rc; } + if (rc != ReturnStatus::success) { + return rc; + } rc = ValidatorUtils::validate_ip4_address(settings->destination_ips); - if (rc != ReturnStatus::success) { return rc; } + if (rc != ReturnStatus::success) { + return rc; + } rc = ValidatorUtils::validate_ip4_port(settings->destination_ports); - if (rc != ReturnStatus::success) { return rc; } + if (rc != ReturnStatus::success) { + return rc; + } return ReturnStatus::success; } @@ -339,35 +363,55 @@ ReturnStatus RivermaxRTPReceiverQueueValidator::validate( const std::shared_ptr& settings) const { auto validator = std::make_shared(); ReturnStatus rc = validator->validate(settings); - if (rc != ReturnStatus::success) { return rc; } + if (rc != ReturnStatus::success) { + return rc; + } if (settings->split_boundary == 0 && settings->gpu_direct) { HOLOSCAN_LOG_ERROR("GPU Direct is supported only in header-data split mode"); return ReturnStatus::failure; } rc = ValidatorUtils::validate_ip4_address(settings->source_ip); - if (rc != ReturnStatus::success) { return rc; } + if (rc != ReturnStatus::success) { + return rc; + } rc = ValidatorUtils::validate_ip4_address(settings->local_ip); - if (rc != ReturnStatus::success) { return rc; } + if (rc != ReturnStatus::success) { + return rc; + } rc = ValidatorUtils::validate_ip4_address(settings->destination_ip); - if (rc != ReturnStatus::success) { return rc; } + if (rc != ReturnStatus::success) { + return rc; + } rc = ValidatorUtils::validate_ip4_port(settings->destination_port); - if (rc != ReturnStatus::success) { return rc; } + if (rc != ReturnStatus::success) { + return rc; + } return ReturnStatus::success; } ReturnStatus RivermaxCommonTxQueueValidator::validate( const std::shared_ptr& settings) const { ReturnStatus rc = ValidatorUtils::validate_core(settings->master_core); - if (rc != ReturnStatus::success) { return rc; } + if (rc != ReturnStatus::success) { + return rc; + } bool res = ConfigManagerUtilities::validate_cores(settings->cpu_cores); - if (!res) { return ReturnStatus::failure; } + if (!res) { + return ReturnStatus::failure; + } rc = ValidatorUtils::validate_ip4_address(settings->local_ip); - if (rc != ReturnStatus::success) { return rc; } + if (rc != ReturnStatus::success) { + return rc; + } rc = ValidatorUtils::validate_ip4_address(settings->destination_ip); - if (rc != ReturnStatus::success) { return rc; } + if (rc != ReturnStatus::success) { + return rc; + } rc = ValidatorUtils::validate_ip4_port(settings->destination_port); - if (rc != ReturnStatus::success) { return rc; } + if (rc != ReturnStatus::success) { + return rc; + } if (!settings->memory_allocation && settings->memory_registration) { HOLOSCAN_LOG_ERROR( "Register memory option is supported only with application memory allocation"); @@ -385,7 +429,9 @@ ReturnStatus RivermaxMediaSenderQueueValidator::validate( const std::shared_ptr& settings) const { auto validator = std::make_shared(); ReturnStatus rc = validator->validate(settings); - if (rc != ReturnStatus::success) { return rc; } + if (rc != ReturnStatus::success) { + return rc; + } return ReturnStatus::success; } @@ -423,8 +469,8 @@ ReturnStatus RivermaxQueueToIPOReceiverSettingsBuilder::convert_settings( target_settings->sleep_between_operations_us = source_settings->sleep_between_operations_us; target_settings->packet_payload_size = source_settings->max_packet_size; target_settings->packet_app_header_size = source_settings->split_boundary; - (target_settings->packet_app_header_size == 0) ? target_settings->header_data_split = false : - target_settings->header_data_split = true; + (target_settings->packet_app_header_size == 0) ? target_settings->header_data_split = false + : target_settings->header_data_split = true; target_settings->num_of_packets_in_chunk = std::pow(2, std::ceil(std::log2(source_settings->packets_buffers_size))); @@ -486,8 +532,8 @@ ReturnStatus RivermaxQueueToRTPReceiverSettingsBuilder::convert_settings( target_settings->sleep_between_operations_us = source_settings->sleep_between_operations_us; target_settings->packet_payload_size = source_settings->max_packet_size; target_settings->packet_app_header_size = source_settings->split_boundary; - (target_settings->packet_app_header_size == 0) ? target_settings->header_data_split = false : - target_settings->header_data_split = true; + (target_settings->packet_app_header_size == 0) ? target_settings->header_data_split = false + : target_settings->header_data_split = true; target_settings->num_of_packets_in_chunk = std::pow(2, std::ceil(std::log2(source_settings->packets_buffers_size))); @@ -541,8 +587,8 @@ ReturnStatus RivermaxQueueToMediaSenderSettingsBuilder::convert_settings( target_settings->print_parameters = source_settings->print_parameters; target_settings->sleep_between_operations = source_settings->sleep_between_operations; target_settings->packet_app_header_size = source_settings->split_boundary; - (target_settings->packet_app_header_size == 0) ? target_settings->header_data_split = false : - target_settings->header_data_split = true; + (target_settings->packet_app_header_size == 0) ? target_settings->header_data_split = false + : target_settings->header_data_split = true; target_settings->stats_report_interval_ms = source_settings->stats_report_interval_ms; target_settings->register_memory = source_settings->memory_registration; From 8df7a2f766db5d6c307967e32f8d9aa79a1f0ec6 Mon Sep 17 00:00:00 2001 From: Rony Rado Date: Wed, 5 Nov 2025 11:24:09 +0000 Subject: [PATCH 6/6] feat(advanced_network) Update Dockerfile - Install holoscan python SDK - Install libvulkan for holoviz operator - Install xvfb and x11vnc for headless servers Signed-off-by: Rony Rado --- operators/advanced_network/Dockerfile | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/operators/advanced_network/Dockerfile b/operators/advanced_network/Dockerfile index 8229525c1d..994c26c5d5 100644 --- a/operators/advanced_network/Dockerfile +++ b/operators/advanced_network/Dockerfile @@ -140,8 +140,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ RUN python3 -m pip install --no-cache-dir \ pytest \ pyyaml \ - scapy - + scapy \ + holoscan==${HOLOSCAN_DEB_REMOTE_VERSION} \ + && SITE_PACKAGES=$(python3 -c "import site; print(site.getsitepackages()[0])") \ + && chmod -R a+w ${SITE_PACKAGES}/holoscan-*.dist-info/ \ + && chmod -R a+rw ${SITE_PACKAGES}/holoscan # ============================== # DOCA Target # This stage is only built when --target doca is specified. It contains any DOCA-specific configurations. @@ -192,6 +195,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libglew-dev \ cuda-nvml-dev-12-6 \ cuda-nvtx-12-6 \ + libvulkan1 \ + xvfb \ + x11vnc \ && rm -rf /var/lib/apt/lists/* # Copy and extract the Rivermax SDK