diff --git a/applications/CMakeLists.txt b/applications/CMakeLists.txt index 380abe6f31..e6d544dfc7 100644 --- a/applications/CMakeLists.txt +++ b/applications/CMakeLists.txt @@ -17,6 +17,12 @@ 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) + add_holohub_application(aja_video_capture DEPENDS OPERATORS aja_source) 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" + } + } +} 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" + } + } +} 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/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 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..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 @@ -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; } @@ -362,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; @@ -372,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; @@ -407,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; } @@ -518,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; } @@ -546,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(); @@ -563,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(); @@ -601,6 +620,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..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 @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -47,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; @@ -74,9 +79,7 @@ class NonBlockingQueue : public QueueInterface { while (!queue_.empty()) { queue_.pop(); } } - void stop() override { - stop_ = true; - } + void stop() override { stop_ = true; } }; /** @@ -93,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(); @@ -102,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; @@ -111,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; @@ -130,8 +139,8 @@ class BlockingQueue : public QueueInterface { } void stop() override { - stop_ = true; - cond_.notify_all(); + stop_ = true; + cond_.notify_all(); } }; @@ -236,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; @@ -277,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; @@ -394,8 +403,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,19 +413,185 @@ 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() { - 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(); + + 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..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 @@ -20,6 +20,9 @@ #include #include +#include +#include +#include #include @@ -84,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; } @@ -137,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; @@ -161,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. @@ -311,9 +316,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. * @@ -389,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; } @@ -400,6 +420,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 +494,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 +551,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 +575,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 +613,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..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_; @@ -546,6 +561,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; } @@ -631,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); @@ -653,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"); @@ -760,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 ed25652c4d..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) { @@ -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"); @@ -95,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; } @@ -106,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; } @@ -125,6 +147,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; } @@ -155,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 << " |" @@ -194,6 +227,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; } @@ -282,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; } @@ -525,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_); } @@ -536,13 +582,25 @@ 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() { - 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(); } + { + 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(); + } } MediaSenderZeroCopyService::MediaSenderZeroCopyService( @@ -576,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; } @@ -611,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( @@ -642,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 063fef8027..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 @@ -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; }; /** @@ -507,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 700e1f9e9e..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 @@ -45,11 +45,17 @@ 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) { - 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; @@ -68,6 +74,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; } @@ -82,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; @@ -102,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; @@ -134,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; @@ -170,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; @@ -281,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; } @@ -292,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"); @@ -317,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; } @@ -331,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"); @@ -377,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; } @@ -415,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))); @@ -432,6 +486,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; @@ -471,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))); @@ -483,6 +544,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; @@ -520,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; 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; }; 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_