Skip to content

Commit 3c5ccdd

Browse files
committed
sensing: add the SENSING SG8A SIPL camera plugin
New C++ plugin camera_plugin_sensing for the SENSING SG8A-AGON-G2Y-A1 GMSL carrier on a Jetson AGX Orin (JetPack 7.2.1 / L4T R39.2.1). It captures two Astra SHW5G at 2560x1984@60 through SIPL and either encodes H.264 on the V4L2 M2M engine or publishes frames over the CUDA IPC transport. The carrier ships no kernel sensor drivers and no /dev/video*: SIPL owns the sensors from userspace, so there is no daemon, no per-boot module load and no V4L2 controls. Capture runs as the ordinary container user, needing only the video, i2c and gpio groups. Four SIPL requirements look arbitrary and are not -- call ordering around Init(), mandatory ICP buffers, NeedCpuAccess on the producer attribute list, and REC709_ER on ISP0. Each is commented where it is enforced; getting any of them wrong fails silently or with a status code that names nothing. sensing.rst and the plugin README carry the rig details. shw5g.json is a verbatim copy of the vendor platform config; configs/README.md records its provenance and the rule against editing it in place. Auto-exposure saturates with sensor gain pinned at 1.0. That reproduces in NVIDIA's own nvsipl_camera, so it is upstream; tracked in #1076. Signed-off-by: Jiwen Cai <jiwenc@nvidia.com>
1 parent e263775 commit 3c5ccdd

28 files changed

Lines changed: 3796 additions & 19 deletions

‎CMakeLists.txt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ option(BUILD_PLUGIN_OAK_CAMERA "Build OAK camera plugin (requires vcpkg for Dept
6363
option(BUILD_PLUGIN_NOITOM_MOCAP "Build Noitom mocap plugin (downloads MocapApi SDK)" OFF)
6464
option(BUILD_PLUGIN_OGLO "Build OGLO tactile glove plugin (BLE, Linux only; fetches SimpleBLE + nlohmann/json)" OFF)
6565
option(BUILD_PLUGIN_WUJI_GLOVE "Build Wuji glove plugin (requires the wuji_sdk C SDK)" OFF)
66-
option(BUILD_PLUGIN_SENSING "Build SENSING GMSL camera plugin (Jetson only)" OFF)
66+
option(BUILD_PLUGIN_SENSING "Build SENSING GMSL camera plugin (Jetson only; requires SIPL + NVENC)" OFF)
6767
option(BUILD_EXAMPLES "Build examples" ON)
6868
option(BUILD_EXAMPLE_TELEOP_ROS2 "Build only the teleop_ros2 ROS 2 reference integration (e.g. for Docker)" OFF)
6969
option(BUILD_TESTING "Build unit tests" ON)
@@ -205,7 +205,7 @@ if(BUILD_PLUGINS)
205205
if(BUILD_PLUGIN_WUJI_GLOVE)
206206
add_subdirectory(src/plugins/wuji_glove)
207207
endif()
208-
# Gated: Jetson only — the Tegra CUDA and NVENC entry points are L4T-only.
208+
# Gated: Jetson only — SIPL and the Tegra NVENC entry point are L4T-only.
209209
if(BUILD_PLUGIN_SENSING)
210210
add_subdirectory(src/plugins/sensing)
211211
endif()

‎docs/source/device/sensing.rst‎

Lines changed: 160 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ SENSING GMSL Camera
55
===================
66

77
Bring-up for SENSING GMSL2 cameras on a Jetson AGX Orin, where capture goes
8-
through **SIPL** rather than Argus or V4L2. This page covers provisioning the
9-
rig and confirming it streams with the vendor's own tool; scripts live in
10-
:code-file:`src/plugins/sensing`.
8+
through **SIPL** rather than Argus or V4L2, and ``camera_plugin_sensing``, the
9+
C++ plugin that captures from them — encoding H.264 on the Jetson's V4L2 engine
10+
or handing frames to another process as CUDA device memory. Source and scripts
11+
live in :code-file:`src/plugins/sensing`.
1112

1213
.. contents:: On this page
1314
:local:
@@ -283,6 +284,151 @@ capture:
283284
nvsipl_query -t query/sg8a_agth_g2a/shw5g.json -l # config names
284285
nvsipl_query -t query/sg8a_agth_g2a/shw5g.json -c SHW5G_2
285286
287+
Building and running
288+
--------------------
289+
290+
.. code-block:: bash
291+
292+
cmake -B build -DBUILD_PLUGIN_SENSING=ON
293+
cmake --build build --target camera_plugin_sensing
294+
295+
The platform config is vendored at ``src/plugins/sensing/configs/shw5g.json``
296+
and resolved relative to the executable, so the plugin runs without the vendor
297+
package on disk; ``--platform-config=PATH`` points it at a newer vendor drop.
298+
299+
.. note::
300+
301+
The published package has **no** ``SHW5G_2`` config — only the mixed
302+
populations. SENSING sends ``shw5g.json`` directly to customers with a
303+
2× SHW5G rig, so the vendored copy is the only one a fresh checkout has, and
304+
``setup_host.sh`` drops it into the fetched package for ``nvsipl_camera``.
305+
306+
Start by asking the platform config what exists. This needs no hardware — the
307+
query API only parses the driver database and the JSON, so it works with the
308+
cameras unplugged:
309+
310+
.. code-block:: bash
311+
312+
./build/src/plugins/sensing/app/camera_plugin_sensing --list-sensors
313+
314+
.. code-block:: text
315+
316+
SHW5G_2: 2 sensor(s)
317+
sensor=0 SHW5G 2560x1984 @ 60 fps
318+
sensor=1 SHW5G 2560x1984 @ 60 fps
319+
320+
.. warning::
321+
322+
``sensor=N`` is the **SIPL pipeline index**, and for ``SHW5G_2`` it is not
323+
the number the JSON appears to name. The GMSL link indices, the CSI virtual
324+
channels and the JSON's ``sensorInfo.id`` are all **2 and 3**; the pipeline
325+
indices are **0 and 1**. Always take the number from ``--list-sensors``.
326+
327+
In the vendor's ``S56C_1_SHF3L_2`` config the two coincide, which is exactly
328+
why this is worth stating.
329+
330+
Geometry comes from the platform config
331+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
332+
333+
There is no ``--width``, ``--height``, ``--fps`` or ``--sensor-mode``. SIPL has
334+
no runtime mode index; resolution and frame rate are properties of the virtual
335+
channel in the platform config, and the plugin sizes its buffers from what the
336+
query reports.
337+
338+
Encoding at 5 MP60
339+
~~~~~~~~~~~~~~~~~~
340+
341+
Two sensors at 2560×1984 @ 60 is 4.9× the pixel rate the 1080p30 defaults were
342+
chosen for, so the codec settings moved with it:
343+
344+
.. list-table::
345+
:widths: 25 20 55
346+
:header-rows: 1
347+
348+
* - Setting
349+
- Value
350+
- Why
351+
* - level
352+
- **5.2**
353+
- 1,190,400 MB/s exceeds Level 5.1's 983,040 by 21%
354+
* - ``--bitrate``
355+
- 40 Mbps
356+
- 0.13 bits/pixel; 20 Mbps was 0.066
357+
* - ``--peak-bitrate``
358+
- 60 Mbps
359+
- VBR ceiling; pass ``0`` to select CBR
360+
361+
80 Mbps for the pair, about 36 GB/hour.
362+
363+
Live streaming with camera_viz
364+
------------------------------
365+
366+
``ipc=<socket>`` serves a sensor's frames to another process as **CUDA device
367+
memory** — RGBA8, no encode, no host round-trip — and
368+
:code-file:`camera_viz <examples/camera_viz/README.md>` consumes it with
369+
``type: cuda_ipc``. It is independent of ``output=``; an ``ipc``-only stream
370+
never starts an encoder.
371+
372+
.. code-block:: bash
373+
374+
# terminal 1 — producer
375+
./build/src/plugins/sensing/app/camera_plugin_sensing \
376+
--add-stream=sensor=0,ipc=/tmp/sensing0.sock \
377+
--add-stream=sensor=1,ipc=/tmp/sensing1.sock
378+
379+
# terminal 2 — viewer
380+
cd examples/camera_viz
381+
./camera_viz.sh setup # one-time
382+
./camera_viz.sh run configs/cuda_ipc.yaml --mode window # desktop window
383+
./camera_viz.sh run configs/cuda_ipc.yaml # XR headset
384+
385+
:code-file:`configs/cuda_ipc.yaml <examples/camera_viz/configs/cuda_ipc.yaml>`
386+
needs only the socket path and the frame size:
387+
388+
.. code-block:: yaml
389+
390+
cameras:
391+
- name: left
392+
type: cuda_ipc
393+
socket: /tmp/sensing0.sock # must match the producer's ipc= path
394+
width: 2560 # must match what the producer serves
395+
height: 1984
396+
397+
Order does not matter — the source retries until the socket appears and survives
398+
the producer restarting under it. ``width``/``height`` are checked during the
399+
handshake and a mismatch is refused rather than rendered at the wrong stride.
400+
**One consumer at a time**: the producer serves whoever connected most recently,
401+
so a second viewer silently takes the feed from the first.
402+
403+
Pairing the two eyes
404+
~~~~~~~~~~~~~~~~~~~~
405+
406+
Both sensors are driven from one deserializer fsync generator, and every frame
407+
carries SIPL's ``frameCaptureTSC`` on a timebase shared across the whole rig.
408+
It is recorded in the MCAP metadata as ``capture_tsc_ns``.
409+
410+
.. important::
411+
412+
Pair on ``capture_tsc_ns``, not on the wrapper timestamps. The wrapper stamps
413+
are ``CLOCK_MONOTONIC`` taken after per-sensor conversion, so they carry each
414+
sensor's own queueing jitter; the TSC does not.
415+
416+
Testing without a camera
417+
~~~~~~~~~~~~~~~~~~~~~~~~
418+
419+
``sensing_ipc_testsrc`` publishes an animated pattern over the same protocol. It
420+
needs CUDA only — no SIPL, no encoder — so the viewer can be developed with
421+
nothing attached:
422+
423+
.. code-block:: bash
424+
425+
cmake --build build --target sensing_ipc_testsrc
426+
./build/src/plugins/sensing/tools/sensing_ipc_testsrc --socket=/tmp/sensing0.sock \
427+
--width=2560 --height=1984 --fps=60
428+
429+
Each frame carries its 16-bit frame number as a binary bar across the top, so a
430+
stale or torn frame is visible rather than merely suspected.
431+
286432
Troubleshooting
287433
---------------
288434

@@ -300,8 +446,12 @@ Troubleshooting
300446
- vendor ``install.sh`` was never run — re-run ``setup_host.sh``
301447
* - ``Failed to open file .../shw5g.json``
302448
- the fetched package ships no ``SHW5G_2`` config; ``setup_host.sh`` drops one in
449+
* - ``sensor=N is not a pipeline in '<config>'``
450+
- using the link index or the JSON id; run ``--list-sensors``
303451
* - capture hangs, no frames, no error
304452
- another SIPL client holds the hardware — stop ``nvsipl_camera``
453+
* - ``ISP0 reconciled to colour standard ...``
454+
- the ISP refused BT.601; see the note in ``sipl_camera.cpp``
305455
* - ``Could not get EglImage from fd`` / ``Failed to create EGLImage``
306456
- ``DISPLAY`` names an X server Tegra EGL cannot drive; see below
307457

@@ -319,4 +469,10 @@ The container defaults to ``DISPLAY=:99``, so anything that renders needs:
319469

320470
.. code-block:: bash
321471
322-
export DISPLAY=:1 # nvsipl_camera --egl-display, or any viewer
472+
export DISPLAY=:1 # nvsipl_camera --egl-display, camera_viz, any viewer
473+
474+
``camera_plugin_sensing`` is unaffected: it never renders and calls
475+
``unsetenv("DISPLAY")`` before its first EGL call, which selects the Tegra
476+
driver regardless. Do not remove that because the capture path "has no
477+
display" — ``NvBufSurfaceMapEglImage()`` is how a SIPL buffer reaches CUDA, so
478+
EGL is on the critical path even headless.

‎examples/camera_viz/README.md‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ SPDX-License-Identifier: Apache-2.0
2121
| `oakd` | OAK-D RGB / LEFT / RIGHT; mono or `stereo: true` (GRAY8 over USB, GPU-broadcast to RGBA; `stereo_rgb` for color). Needs the Luxonis udev rule — see below |
2222
| `zed` | ZED 2 / Mini / X One; mono or `stereo: true` (per-eye SDK retrieve, zero-copy GPU) |
2323
| `video` | Video-file replay (anything OpenCV/FFmpeg reads) — preview / testing without a camera. Loops by default; `stereo: true` splits side-by-side files into eyes (viewer only) |
24-
| `cuda_ipc` | RGBA8 frames mapped straight out of another process's CUDA memory — no encode, no host copy. See [below](#cuda_ipc-frames-from-another-process) |
24+
| `cuda_ipc` | RGBA8 frames mapped straight out of another process's CUDA memory — no encode, no host copy. Pairs with the [sensing plugin](../../docs/source/device/sensing.rst); see [below](#cuda_ipc-frames-from-another-process) |
2525

2626
In XR mode the viewer **attaches to the CloudXR runtime + WSS proxy**, starting a background service if none is serving — nothing to start separately (`--accept-eula` for the first run; `camera_viz.py --help` for the rest). Output: XR headset (default) or desktop window (`run CONFIG --mode window`); one surface per camera — a flat plane (default), a cylinder arc, or an equirect sphere (`placements.<name>.shape`, XR only for the curved shapes). Stereo cameras render true SBS in XR; window mode shows the left eye. XR placements: `world` / `head` / `lazy` / `gimbal`.
2727

@@ -164,9 +164,18 @@ survives the producer restarting under it. `width`/`height` are checked during
164164
the handshake and a mismatch is refused rather than rendered at the wrong
165165
stride.
166166

167-
Any process can be the producer — the wire format is a 24-byte message and a
168-
CUDA VMM handle over `SCM_RIGHTS`. An animated test pattern ships with it, so
169-
the consumer can be developed and tested with no camera and no capture SDK:
167+
Producer is the sensing plugin. Geometry comes from its platform config, not
168+
from flags, and `sensor=` is the **SIPL pipeline index** — take it from
169+
`--list-sensors`, not from the vendor JSON, where the same cameras are numbered
170+
differently:
171+
172+
```bash
173+
camera_plugin_sensing --add-stream=sensor=0,ipc=/tmp/s0.sock \
174+
--add-stream=sensor=1,ipc=/tmp/s1.sock
175+
```
176+
177+
To develop against this without a camera, the plugin ships an animated test
178+
pattern that speaks the same protocol:
170179

171180
```bash
172181
cmake --build build --target sensing_ipc_testsrc

‎src/core/schema/fbs/sensing.fbs‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
include "timestamp.fbs";
5+
6+
namespace core;
7+
8+
// Per-frame metadata pushed by the SENSING camera plugin (one per stream).
9+
// Streams are keyed by SIPL pipeline index rather than an enum: the populated
10+
// set varies with the platform config the rig is running.
11+
// All fields are always present when the parent Tracked/Record wrapper's data is non-null.
12+
table FrameMetadataSensing {
13+
// SIPL pipeline index, as INvSIPLCameraQuery reports it for the active
14+
// platform config. This is NOT the GMSL link index, the CSI virtual channel,
15+
// or the JSON sensorInfo.id -- for SHW5G_2 those are 2 and 3 while this is
16+
// 0 and 1.
17+
sensor_id: uint32 (id: 0);
18+
19+
// Per-stream counter of records emitted for this sensor. Not a SIPL capture
20+
// index: a stream with an encoder counts bitstream units, one per captured
21+
// frame, and an ipc-only stream counts published frames.
22+
sequence_number: uint64 (id: 1);
23+
24+
// SIPL frameCaptureTSC: capture time on the Tegra TSC timebase, which every
25+
// sensor on the rig shares because the deserializer drives them from one
26+
// fsync generator. This is the field that lets a consumer pair the two eyes
27+
// of a stereo rig; the wrapper timestamps cannot, since they are stamped
28+
// after per-sensor conversion. Zero when the encoder dropped the frame this
29+
// record's stamp came from.
30+
capture_tsc_ns: uint64 (id: 2);
31+
}
32+
33+
// MCAP recording wrapper for FrameMetadataSensing.
34+
table FrameMetadataSensingRecord {
35+
data: FrameMetadataSensing (id: 0);
36+
timestamp: DeviceDataTimestamp (id: 1);
37+
}
38+
39+
root_type FrameMetadataSensingRecord;

0 commit comments

Comments
 (0)