Skip to content

sensing: add a mock backend so the plugin runs without a Jetson - #1340

Open
gareth-morgan-nv wants to merge 10 commits into
jiwenc/camera-cudafrom
gmorgan/sensing-mock-backend
Open

gareth-morgan-nv wants to merge 10 commits into
jiwenc/camera-cudafrom
gmorgan/sensing-mock-backend

Conversation

@gareth-morgan-nv

@gareth-morgan-nv gareth-morgan-nv commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds a MOCK backend to the SENSING camera plugin so it builds and runs on any machine with an NVIDIA GPU, not only on a Jetson. The mock replaces only SiplCamera and JetsonEncoder; everything above them (SensingCamera, the sinks, the YUV-to-RGBA and RGBA-to-NV12 kernels, and the CUDA IPC publisher) is the code that runs on the rig. It also fixes the capture timestamp of encoded streams, which was always zero.

Stacked on the SENSING camera plugin PR (#998): the base branch is its head branch, so this diff shows only the changes below.

What changed

  • SENSING_BACKEND (AUTO, JETSON, MOCK). AUTO picks JETSON on aarch64 and MOCK elsewhere, so a Jetson without the SDK still gets the existing setup instructions instead of a silent mock. The Jetson SDK checks are unchanged apart from sitting inside the JETSON branch.
  • Mock camera. Answers --list-sensors and the link masks from the vendor JSON, including the renumbering of sensors to pipeline indices. Builds a synthetic NV12 surface from an animated GIF and runs the real conversion on it. Keeps the three-slot mailbox with its reader lease and a capture TSC shared by every sensor. SENSING_MOCK_FPS, SENSING_MOCK_FAIL_AFTER and SENSING_MOCK_SCALE_DIV control frame rate, an injected link failure and test-size frames.
  • GIF source. Each sensor shows an animated GIF fixed by its module name: S56C_HEAD the robot (its two sensors are the left and right eye, offset against each other by an illustrative fixed disparity), SHF3L_LEFT the waving hand, SHF3L_RIGHT the hand flipped left to right. It plays on the capture clock, scaled to cover the sensor's configured size and centre-cropped, never stretched. A module with no picture, or a GIF that cannot be loaded, is an error; there is no fallback. mock_assets/mock_rig.json is a mock-only three-camera rig (two 2560x1984 head eyes, two 1920x1536 wrists).
  • Mock encoder. Runs the real RGBA-to-NV12 kernel and device-to-host copy, then encodes with the desktop GPU's NVENC through libavcodec's h264_nvenc, set up as the Jetson encoder is: High profile, Level 5.2, VBR with a peak, an IDR every GOP with SPS/PPS in front of it, no B-frames. The output is real, decodable H.264. It keeps the real encoder's contract: a fixed pool of input buffers that makes submit() refuse frames when behind, microsecond-resolution stamps, and a concatenated tail at end of stream. Bitstream bytes, unit sizes and latency are NVENC's, not the Jetson engine's. Needs libavcodec-dev and a driver with NVENC.
  • Timestamp fix. The encoder returns each unit's stamp truncated to microseconds, while SensingCamera matched it against the submitted nanosecond stamp for equality, so capture_tsc_ns was 0 in the metadata of every encoded stream. The comparison is now at microsecond resolution (own commit).
  • Bundled GIFs (robot and waving hand) are Animated Noto Emoji, copyright Google, licensed CC BY 4.0, unmodified, with the licence, source and attribution in mock_assets/. LICENSES/CC-BY-4.0.txt is the canonical text and the root README notes the exception. They are stored through Git LFS. Decoding uses stb_image, a public-domain header fetched at configure time at a pinned commit and checked against its hash; only the mock backend uses it.
  • Tests (sensing_mock_tests): the camera, the encoder, SensingCamera end to end, and a CUDA IPC consumer that maps the shared allocation and reads published frames. GIF scaling is covered for upscaling, downscaling and mismatched aspect ratios, at native size and at test size. The existing test_sipl_query also runs against the mock.

Validation

  • sensing_mock_tests: 37 cases pass; the encoder tests decode the output with libavcodec on a laptop with an NVIDIA GPU (CUDA 12.0 toolkit).
  • test_sipl_query passes against the mock.
  • The camera_plugin_sensing binary run with defaults captured about 220 frames per sensor in 4 seconds and wrote a metadata file.
  • clang-format-14 --dry-run --Werror is clean on every C++ and CUDA file changed.
  • pre-commit over the changed files: 10 hooks pass. The REUSE hook still reports pre-existing SPDX-like string literals in scripts/check_licenses.py, src/core/codegen/generate_trackers.py and src/core/codegen/templates.py, none of which this PR touches; nothing in the files changed here is flagged.
  • Forcing SENSING_BACKEND=JETSON on x86 stops at the existing "SIPL headers not found" error.
  • Not run: the JETSON backend was never built or run, and nothing was exercised against SIPL, the V4L2 encoder or real cameras.

Known limitations / follow-ups

  • A pass on the mock says nothing about SIPL, the ISP output format, NvSci buffer lifetime, the V4L2 encoder or real frame rates; those need an Orin with SENSING cameras.
  • The mock needs an NVIDIA GPU, the CUDA toolkit and the EGL headers (libegl-dev), libavcodec-dev with a driver that has NVENC, and fetches stb_image at configure time.
  • The mock encoder is NVENC, not the Jetson V4L2 engine, so bitrate behaviour and latency differ from the rig.
  • The JETSON build files were restructured (wrapped in the backend switch) without being built here.

Test plan

  • On an Orin with SENSING cameras: configure and build with the default JETSON backend and confirm the plugin builds and captures as before.
  • On a machine with an NVIDIA GPU: cmake -B build -DBUILD_PLUGIN_SENSING=ON -DSENSING_BACKEND=MOCK, then run sensing_mock_tests and camera_plugin_sensing --list-sensors.
  • Review the timestamp fix on its own: sensing_camera.cpp.
  • CI.

PR description drafted with Claude Sonnet 5.5.

gareth-morgan-nv and others added 4 commits October 24, 2026 17:50
Add SENSING_BACKEND (AUTO, JETSON, MOCK). AUTO keeps JETSON on aarch64, so a
Jetson missing the SDK still gets the setup instructions, and falls back to
MOCK elsewhere.

The mock replaces only SiplCamera and JetsonEncoder. Everything above them is
the code that runs on the rig: SensingCamera, the sinks, the YUV-to-RGBA and
RGBA-to-NV12 kernels, and the CUDA IPC publisher.

- SiplCamera: answers query() from the vendor JSON, including the renumbering
  of sensors to pipeline indices and the per-deserializer link masks; paints a
  synthetic NV12 surface and runs the real conversion on it; keeps the
  three-slot mailbox with its reader lease and a capture TSC shared by every
  sensor. Fault injection and scaling through SENSING_MOCK_* variables.
- JetsonEncoder: runs the real RGBA-to-NV12 kernel and the device-to-host
  copy, then emits Annex-B shaped units (not decodable video) with the real
  encoder's contract: six input buffers that make submit() refuse frames when
  behind, a warm-up delay, microsecond-resolution stamps, and a concatenated
  tail at end of stream.

Tests (sensing_mock_tests) cover the camera, the encoder, SensingCamera end to
end, and a CUDA IPC consumer that maps the shared allocation and reads the
published frames. The existing test_sipl_query now also runs against the mock.

One test is marked [!shouldfail]: the capture TSC of an encoded stream is never
recovered, because the encoder returns stamps at microsecond resolution and
SensingCamera matches them for nanosecond equality.

Nothing here has been run against SIPL or the V4L2 encoder.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
The V4L2 encoder carries the capture stamp on a timeval, so a unit comes back
with the stamp truncated to microseconds. SensingCamera remembers the stamp
it submitted in nanoseconds and looked the unit up by equality, so the match
essentially never succeeded: every metadata record of an encoded stream had
capture_tsc_ns == 0, and each failed lookup also discarded the pending stamps
ahead of it.

Compare at microsecond resolution. The mock encoder reproduces the
truncation, and the test that recovers the capture TSC of an encoded stream no
longer needs [!shouldfail].

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
- Free a mock sensor's device memory from its own destructor, so a constructor
  that throws part-way through allocating does not leak.
- Abandon queued frames when the mock encoder is torn down instead of
  encoding them, which made teardown take as long as the queue was deep.
- Treat a link index of 8 or more as disabled; a mask holds eight hex digits.
- Fail configure with an install hint when the EGL headers cudaEGL.h needs are
  missing, and name them in the README.
- Remove a compile definition nothing used, and a stale comment in tools/.
- Remove the tests' temp files on every exit path.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
Every mock sensor shows an animated GIF, by default the bundled robot. Set
SENSING_MOCK_GIF for every sensor, or SENSING_MOCK_GIF_<module name> for one
(for example the bundled waving hand on a wrist camera). A GIF that cannot be
loaded, the bundled one included, is an error; there is no fallback.

The GIF is decoded once, held on the GPU, and played on the capture clock, with
each frame's delay clamped to at least 20 ms and the animation looping.
Transparent pixels are composited onto a plain gray. Frames are scaled to cover
the sensor's configured size and centre-cropped, so the aspect ratio is never
distorted whatever the GIF's own size: bilinear when enlarging and
area-averaged when reducing. The result is full-range BT.709 NV12, the inverse
of the matrix the real conversion applies, so it goes through that conversion
unchanged. Sequence numbers and stamps advance on every capture even while the
GIF shows one image.

The bundled GIFs are Animated Noto Emoji (robot U+1F916, waving hand U+1F44B),
copyright Google, licensed CC BY 4.0, included unmodified with their licence,
source and attribution in mock_assets/. LICENSES/CC-BY-4.0.txt is the canonical
text, and the root README notes the exception.

Decoding uses stb_image, a public-domain header fetched at a pinned commit and
checked against its hash at configure time. The mock backend no longer paints a
test pattern. Tests use small generated GIFs of known colours and check
sequence, stamps, whole (untorn) frames, the reader lease and the scaling.

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

coderabbitai Bot commented Oct 24, 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

Comment @coderabbitai help to get the list of available commands.

gareth-morgan-nv and others added 6 commits October 25, 2026 00:00
Add a mock rig config (S56C stereo head camera, two SHF3L wrist cameras)
with each camera's real size and aspect. Pictures are chosen by module
name, not pipeline index: the head eyes show the robot offset by a fixed
stereo disparity, the right wrist is the left wrist's picture flipped.
A module with no known picture must be given one by the environment.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
Pictures are fixed by module name, so the per-module and global GIF
overrides and the flip knob only added precedence rules and tests. Tests
that need a known picture use an internal hook instead.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
The rig tests cover the fixed pictures, the wrist mirror and masking.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
Feed the NV12 buffer to libavcodec's h264_nvenc, set up as the Jetson
V4L2 encoder is: High profile, Level 5.2, VBR with a peak, an IDR every
GOP with SPS/PPS in front of it, no B-frames, microsecond stamps. The
tests decode the output. Drops the fake payload generator and its
latency and depth knobs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
Cover the whole constructor with the cleanup guard, check the encoder
option calls, and make the test decoder pad its input and fail on any
decoder error or damaged picture.

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

This branch has not been deployed

No deployments
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