Skip to content

camera_streamer: add SIPL camera support to the sender - #1473

Open
gareth-morgan-nv wants to merge 15 commits into
teng/5ef2afafrom
gmorgan/CameraStreamingSender
Open

gareth-morgan-nv wants to merge 15 commits into
teng/5ef2afafrom
gmorgan/CameraStreamingSender

Conversation

@gareth-morgan-nv

Copy link
Copy Markdown
Contributor

Summary

Adds SIPL camera support to the camera_streamer sender. Each ISP0 frame stays a native NV12 surface and is queued to the hardware HEVC encoder through its DMA-BUF: no RGBA conversion and no CPU pixel copy. The stream uses the existing AVS1/TCP link, so the unchanged receiver decodes it into its usual CUDA IPC buffers.

The branch is three layers, in this order:

  1. A vendor-neutral capture seam, with Argus types removed from the generic encoder and timing code.
  2. The SIPL capture backend and its config.
  3. A shared, SDK-free sender loop that consumes any CameraCapture; SIPL uses it.

What changed

  • CaptureFrame / CameraCapture: an SDK-free contract (camera descriptors, per-camera sequence, session identity, raw sensor time with an explicit clock domain, a native NV12 DMA-BUF surface, and a move-only lease that returns the buffer to its backend).
  • capture_backend: auto | argus | sipl in the config, with a selection function that fails with a clear message for a backend that is not built in (and for auto when more than one is).
  • ArgusFrameAdapter describes an Argus DMA-BUF capture as a CaptureFrame.
  • DmaBuffer no longer carries Argus types: an owner hook (DmaBufferOwner, releaseToOwner) replaces the Argus stream/buffer fields; the Argus pairing and its acquire/release rules move to ArgusBufferBinding. DmaBuffer::createExternal wraps a DMA-BUF the sender does not own.
  • FrameTiming and the sender clock readers move from StereoTiming.h into an SDK-free FrameTiming.h.
  • SiplCapture implements CameraCapture: platform query, ICP and ISP0 buffer allocation, NvSci buffer and sync setup, tuning registration, frame and pipeline-notification threads. A frame replaced before the consumer takes it goes straight back to SIPL.
  • CaptureSender (runCaptureSender): TCP accept and send, AVS1 stream templates and bundle assembly, one HEVC encoder per stream, and for each frame waitForFrame -> acquireLatest -> wrap the DMA-BUF -> bundle registration -> encoder queue, with the lease released when the encoder dequeues the input. A frame that arrives with no free encoder input is dropped rather than waited for. EncoderInputSlots holds the in-flight bookkeeping as a header-only template so it is unit-testable without the encoder SDK. SiplSender is now a thin wrapper that maps streams to SIPL pipelines.
  • Config: a sipl: section (platform JSON and name, link masks, tuning directory, ISP0 buffer count); stream device is a SIPL pipeline index. network.send_buffer_kb (default 0, off) caps the kernel TCP send buffer.
  • WITH_SIPL CMake option (off by default) builds SIPL as a separate C++17 library; the rest of the sender stays C++14 and Argus-only builds are unchanged.

Validation

  • clang-format-14 and pre-commit (CI hook set, copyright-year hook skipped as in CI) clean on the changed files of each layer.
  • aarch64 cross-compile: clang syntax check and a full link of argus_sender, with -DWITH_SIPL=ON and OFF.
  • Unit tests that need no Jetson headers, built and run by hand on a PC: config, capture_backend, avs1_wire, bundle_assembler (stubbed clock readers) and the new encoder_input_slots (lease held while in flight, out-of-order completion, lease dropped before its wrapper, double and out-of-range completion rejected, release-all, release on destruction), including a run under address and undefined-behavior sanitizers.
  • Native build with WITH_SIPL=ON and the camera-free unit tests (17 of 17) on a Jetson Orin (R39.2.1). That run was on a local build that also contained the two PRs stacked after this one, not on this branch alone.
  • A real SIPL stream (one SHW5G camera, 2560x1984 at 30 FPS, HEVC) through the existing camera_receiver and CUDA IPC consumer: about 30 fps decoded with no decode errors at 4 Mbit/s over WiFi. Also run on that combined build: a receiver disconnect and reconnect rebuilt the pipeline and the stream decoded again.
  • The Argus capture path on a camera. The test Orin has only the SIPL rig, so the DmaBuffer change that touches the Argus buffer lifetime has been compiled and linked but not run against an Argus camera.
  • More than one camera: only SIPL pipeline 0 was streamed.

Known limitations / follow-ups

  • The Argus loop (runConfiguredCapture) does not consume CameraCapture and is not changed: its buffers are client-owned encoder DMA-BUFs returned with releaseToOwner, it acquires per capture domain, one Argus bundle can span several streams and stereo pairs, and raw NV12 output has its own copy path. Only the SIPL half goes through the shared sender.
  • The frame-drop decision and the drain, end-of-stream and shutdown order call the encoder directly, so a hardware run covers them, not a unit test.
  • Throughput is link-bound: the test WiFi link carried about 8.4 Mbit/s, so 25 Mbit/s of HEVC backed up for seconds and 4 Mbit/s ran in real time; send_buffer_kb did not change that.
  • Whether the encoder accepts the NvSci-backed block-linear surface through its DMA-BUF descriptor was established by running it on this Orin and driver; no comparison with a VIC copy was made.
  • On a receiver disconnect the NVENC capture plane logs "Timed out waiting for dqthread" and a broken-pipe error before the pipeline rebuilds; the stream recovers.
  • The component directory and binary are still named argus_sender.

Test plan

  • Build with -DWITH_SIPL=ON on an Orin with the SIPL SDK and stream with a platform JSON for your rig: same startup lines, bundles decode, a clean Ctrl-C, and a receiver-disconnect rebuild.
  • Run the Argus smoke test on an Argus-capable board.
  • CI.

🤖 Generated with Claude Code

gareth-morgan-nv and others added 15 commits November 10, 2026 23:13
…ure backend selection

CaptureFrame.h defines the boundary between a camera backend and the encoder
(native NV12 surface, leased buffer, per-camera sequence, session epoch, raw
sensor time with an explicit clock domain). CaptureBackend selects argus, sipl
or auto from the build's available backends; the config gains capture_backend.
No existing capture path is changed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
describeArgusFrame maps the client-owned NV12 DMA-BUF layout, the VI start-of-frame TSC
(as the sensor time, or none when Argus reported none) and the acquire time onto the
vendor-free CaptureFrame, with a lease that returns the buffer to Argus. Not yet used by
StreamProcessor, and not compiled: the Jetson multimedia SDK is not available off the Orin.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
The header promised a two-plane NV12 surface but the check only rejected fewer than
two planes. It now requires NV12 and exactly two planes. Not compiled here: the Jetson
multimedia SDK is not available off the Orin.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
…n id and keep SIPL counters out of TSC ns fields

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
… sender does not own to the encoder

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
Config.cpp parses capture_backend through parseCaptureBackend, so the config test target needs
CaptureBackend.cpp. Found by the native build on the Orin.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
… capture seam backend-neutral

CameraCapture is the interface a capture backend implements (cameras, start, stop, acquireLatest, waitForFrame).
CaptureFrame gains the surface colour range, and its clock domain for a backend counter is named for what it is
(a raw counter in the backend's own units) instead of for one SDK. Comments in the seam no longer name a camera
SDK; only a backend does.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
… of StereoTiming.h

FrameTiming, SenderClockDomain and the sender clock readers move to FrameTiming.h, which includes no camera
SDK header. The AVS1 assembler, the TCP sender, the encoder and the stereo alignment policy include it instead
of StereoTiming.h, which keeps the Argus frame readers and now only pulls in Argus for code that needs it.
StereoAlignmentPolicy.h declared a NULL default and relied on Argus for <cstddef>; it includes it now.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
…maBuffer

DmaBuffer carried an Argus stream and buffer and the release rules for them, so the encoder header included
Argus and every backend's buffers looked like Argus buffers. DmaBuffer now has a neutral owner hook
(DmaBufferOwner, setOwner, releaseToOwner) and its create takes a width and height. The Argus stream/buffer
pair and its acquire/release rules live in ArgusBufferBinding, which only the Argus capture path uses; the
behaviour is unchanged. HevcEncoder.h includes EGL's extension header itself instead of getting it from Argus.

Cross-compiles for aarch64; the Argus path has not been run on a camera.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
SiplCapture adds the SIPL pipeline setup (query, ICP and ISP0 buffer
allocation, NvSci sync, auto-control registration, frame and notification threads) without
the RGBA conversion. Each frame is the ISP0 NvBufSurface described as a CaptureFrame, with
the SIPL capture counter as its sensor time in its own clock domain and a lease that returns
the buffer to SIPL. WITH_SIPL builds it as a C++17 library; the sender stays C++14.

Type-checks and cross-compiles against the Orin sysroot (cross_build.sh). Not run: the
SIPL pipeline has never been started here, and whether the encoder accepts the surface's
DMA-BUF descriptor is unverified.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
…waitForFrame

capture_backend: sipl takes a sipl: section (platform JSON and name, link masks, NITO directory, ISP0
buffer count) and is validated to mono HEVC streams without a remap. SiplCapture::waitForFrame blocks on
a condition variable signalled by the frame and event threads instead of polling.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
runSiplSender feeds each SIPL ISP0 frame to its stream's HEVC encoder as an external DMA-BUF and sends it
as its own AVS1 bundle (independent mono streams); the frame's SIPL buffer is returned when the encoder has
dequeued the input. SenderSession selects the backend from capture_backend; Argus-only builds are unchanged.
Cross-compiles with and without WITH_SIPL; never run on a camera.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
…d buffer

Linux autotunes the send buffer up to several MB, which on a slow link hides seconds of video between the
encoder and the wire. A cap pushes the backlog back to the sender, where the existing queue and the encoder
back-pressure drop frames instead. Default 0 keeps the operating system's behaviour.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
Tracks the encoder input slots that hold a captured frame's DMA-BUF
wrapper and lease, so the backend gets each buffer back only once the
encoder has dequeued it. The wrapper type is a template parameter, so
the slot and lease bookkeeping is unit-tested without the encoder SDK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
…apture

The per-frame path that encodes a CaptureFrame's DMA-BUF in place and
sends it as its own AVS1 bundle now lives in runCaptureSender, which
takes any CameraCapture and names no camera SDK. runSiplSender only
builds the SIPL backend and resolves streams to SIPL pipelines, then
calls it. TSC handling, frame drops when the encoder has no free input,
the drain before EOS and dropping every lease before capture stops are
unchanged.

The Argus path stays in runConfiguredCapture: it captures into
encoder-owned buffers and bundles several cameras and capture domains
per frame, which this single-camera path does not model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
@coderabbitai

coderabbitai Bot commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

This branch had an error being deployed

1 failed deployment
dev — 42a5c257 Deployed Nov 11, 2026 by gareth-morgan-nv via publish-wheel #6761
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants