Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmake/ClangFormat.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ set(_cf_patterns
)

set(_cf_exclude_patterns
".*\\.template$"
".*/build/.*"
".*/deps/.*"
".*/manus/ManusSDK/.*"
Expand Down
46 changes: 46 additions & 0 deletions cmake/GenerateTrackers.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

function(isaac_teleop_generate_trackers)
set(_TRACKER_MANIFEST "${CMAKE_SOURCE_DIR}/src/core/deviceio_trackers/trackers.toml")
set(_TRACKER_DEFAULTS "${CMAKE_SOURCE_DIR}/src/core/deviceio_trackers/defaults.toml")
set(_GENERATOR "${CMAKE_SOURCE_DIR}/src/core/codegen/generate_trackers.py")
set(_CODEGEN_TEMPLATES_DIR "${CMAKE_SOURCE_DIR}/src/core/codegen/templates")
file(GLOB_RECURSE _CODEGEN_IN_TEMPLATES CONFIGURE_DEPENDS
"${_CODEGEN_TEMPLATES_DIR}/*.template")
set(_OUT_DIR "${CMAKE_BINARY_DIR}/generated/trackers")
set(_CMAKE_OUT "${_OUT_DIR}/generated_sources.cmake")

# Pass --prune-stale so renamed/removed trackers cannot leave orphan headers that
# still satisfy stale #includes on the generated include path.
execute_process(
COMMAND "${Python3_EXECUTABLE}" "${_GENERATOR}"
--manifest "${_TRACKER_MANIFEST}"
--defaults "${_TRACKER_DEFAULTS}"
--out-dir "${_OUT_DIR}"
--emit-cmake "${_CMAKE_OUT}"
--prune-stale
RESULT_VARIABLE _tracker_gen_rc
OUTPUT_VARIABLE _tracker_gen_out
ERROR_VARIABLE _tracker_gen_err)
if(NOT _tracker_gen_rc EQUAL 0)
message(FATAL_ERROR "tracker codegen failed:\n${_tracker_gen_out}\n${_tracker_gen_err}")
endif()

include("${_CMAKE_OUT}")
set(GENERATED_TRACKER_DIR "${GENERATED_TRACKER_DIR}" PARENT_SCOPE)
set(GENERATED_TRACKER_FACADE_SOURCES "${GENERATED_TRACKER_FACADE_SOURCES}" PARENT_SCOPE)
set(GENERATED_TRACKER_LIVE_SOURCES "${GENERATED_TRACKER_LIVE_SOURCES}" PARENT_SCOPE)
set(GENERATED_TRACKER_REPLAY_SOURCES "${GENERATED_TRACKER_REPLAY_SOURCES}" PARENT_SCOPE)
set(GENERATED_TRACKER_DEVICEIO_BASE_INC_DIR "${GENERATED_TRACKER_DEVICEIO_BASE_INC_DIR}" PARENT_SCOPE)
set(GENERATED_TRACKER_DEVICEIO_TRACKERS_INC_DIR "${GENERATED_TRACKER_DEVICEIO_TRACKERS_INC_DIR}" PARENT_SCOPE)
set(GENERATED_TRACKER_LIVE_IMPL_INC_DIR "${GENERATED_TRACKER_LIVE_IMPL_INC_DIR}" PARENT_SCOPE)
set(GENERATED_TRACKER_REPLAY_IMPL_INC_DIR "${GENERATED_TRACKER_REPLAY_IMPL_INC_DIR}" PARENT_SCOPE)
set(GENERATED_TRACKER_INC_DIR "${GENERATED_TRACKER_INC_DIR}" PARENT_SCOPE)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
"${_TRACKER_MANIFEST}" "${_TRACKER_DEFAULTS}" "${_GENERATOR}"
"${CMAKE_SOURCE_DIR}/src/core/codegen/manifest.py"
"${CMAKE_SOURCE_DIR}/src/core/codegen/templates.py"
"${CMAKE_SOURCE_DIR}/src/core/codegen/in_renderer.py"
${_CODEGEN_IN_TEMPLATES})
endfunction()
83 changes: 65 additions & 18 deletions docs/source/device/add_device.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,10 @@ The reference implementation is the **generic 3-axis foot pedal**:
* - Device Plugin
- :code-dir:`src/plugins/generic_3axis_pedal`
* - Tracker facade (``Generic3AxisPedalTracker``)
- :code-file:`src/core/deviceio_trackers/cpp/generic_3axis_pedal_tracker.cpp` and
:code-file:`src/core/deviceio_trackers/cpp/inc/deviceio_trackers/generic_3axis_pedal_tracker.hpp`
- Generated at configure time from :code-file:`src/core/deviceio_trackers/trackers.toml`
(see :ref:`Passthrough tracker manifest <passthrough-tracker-manifest>`)
* - Live backend (``LiveGeneric3AxisPedalTrackerImpl``)
- :code-file:`src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.cpp` and
:code-file:`src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.hpp`
- Generated alongside the facade under ``${CMAKE_BINARY_DIR}/generated/trackers/``
* - ``SchemaTracker`` / ``SampleResult``
- :code-file:`src/core/live_trackers/cpp/inc/live_trackers/schema_tracker.hpp`
* - Debug printer
Expand Down Expand Up @@ -107,11 +106,55 @@ Reference implementation: :code-dir:`src/plugins/generic_3axis_pedal`. The plugi
Step 3: Implement a tracker
----------------------------

.. _passthrough-tracker-manifest:

Passthrough trackers (manifest)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

If the device is a **pure FlatBuffer passthrough** over OpenXR tensor collections (plugin serializes
a table; the host reads it unchanged), you usually **do not** hand-write the seven-layer tracker
stack. Instead:

1. Add or extend the FlatBuffer schema (Step 1).
2. Add a ``[[tracker]]`` entry to :code-file:`src/core/deviceio_trackers/trackers.toml`. Defaults
in :code-file:`src/core/deviceio_trackers/defaults.toml` expand ``%name%``, ``%name_CamelCase%``,
MCAP channel names, and class names; override only genuine exceptions (``se3_tracker`` uses
``class = "Se3Tracker"``; pedals use ``schema = "pedals"`` and ``channel = "pedals"``).
3. Reconfigure/rebuild. ``cmake/GenerateTrackers.cmake`` runs
:code-file:`src/core/codegen/generate_trackers.py` and wires generated sources into
``deviceio_trackers``, ``live_trackers``, and ``replay_trackers``.

Use ``direction = "out"`` when Teleop **pushes** a typed table to a plugin (e.g.
``HapticCommandPushTracker``). Cross-process **consumers** that bucket multiple
``HapticCommand.endpoint`` samples on one collection (e.g. ``HapticCommandReaderTracker`` for Manus
haptics) stay hand-written — see :doc:`../references/generated_trackers`.

For what the generator emits, which trackers it covers today, and which shapes are not supported
yet, see :doc:`../references/generated_trackers`.

Diagnose surprising defaults with::

python src/core/codegen/generate_trackers.py \\
--manifest src/core/deviceio_trackers/trackers.toml \\
--defaults src/core/deviceio_trackers/defaults.toml \\
--out-dir /tmp/isaac-teleop-trackers \\
--emit-cmake /tmp/isaac-teleop-trackers.cmake \\
--print-resolved
Comment thread
nv-jakob marked this conversation as resolved.

(``--out-dir`` and ``--emit-cmake`` are required by the CLI even when ``--print-resolved``
skips writing generated sources; temporary paths are fine.)

Hand-written trackers
~~~~~~~~~~~~~~~~~~~~~

Non-passthrough trackers (OpenXR ``xrLocate*``, multi-stream camera metadata, opaque message
channels, …) still use the manual facade + live/replay impl pattern below.

Comment thread
nv-jakob marked this conversation as resolved.
The tracker runs inside a consumer process (e.g. Teleop pipeline or a small reader app). It
implements the **ITracker** interface (tracker **facade** in ``deviceio_trackers``); the
**live backend** in ``live_trackers`` composes **SchemaTracker** to read raw
tensor samples from OpenXR. Implement a concrete tracker class (e.g.
``Generic3AxisPedalTracker``) that:
``HandTracker``) that:

- **Extends ITracker** — Override ``get_name()``,
``get_schema_name()``, ``get_schema_text()``, and ``get_record_channels()``.
Expand Down Expand Up @@ -141,19 +184,23 @@ In the **Impl**:
device disappeared and there are no samples, you may emit one record with null data and
the update-tick timestamp so the MCAP stream marks absence.

Reference implementation — split across facade and live backend:

- **Tracker facade** — :code-file:`src/core/deviceio_trackers/cpp/generic_3axis_pedal_tracker.cpp`
(class ``Generic3AxisPedalTracker``): holds collection configuration, implements ``ITracker``, and
exposes ``get_data(session)`` returning
``Generic3AxisPedalOutputTrackedT`` by dispatching to the session’s
``IGeneric3AxisPedalTrackerImpl`` (see :code-file:`src/core/deviceio_base/cpp/inc/deviceio_base/generic_3axis_pedal_tracker_base.hpp`).
- **Live backend** — :code-file:`src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.cpp`
(``LiveGeneric3AxisPedalTrackerImpl``): composes ``SchemaTracker``, implements ``update()`` and
``serialize_all()``, and uses ``SchemaTracker::read_all_samples()`` with
``std::vector<SchemaTracker::SampleResult>`` for the pending batch. See
:code-file:`src/core/live_trackers/cpp/inc/live_trackers/schema_tracker.hpp`
for ``SchemaTracker`` and ``SampleResult`` (buffer + timestamp metadata).
Reference implementation — SchemaTracker passthrough (generated) and a hand-written OpenXR
locate tracker:

- **Generated SchemaTracker passthrough** — after configure, under
``${CMAKE_BINARY_DIR}/generated/trackers/``: facade
``deviceio_trackers/generic_3axis_pedal_tracker.cpp`` (``Generic3AxisPedalTracker``), base
``deviceio_base/generic_3axis_pedal_tracker_base.hpp``, and live backend
``live_trackers/live_generic_3axis_pedal_tracker_impl.cpp``
(``LiveGeneric3AxisPedalTrackerImpl``). The live impl composes ``SchemaTracker`` and uses
``read_all_samples()`` with ``std::vector<SchemaTrackerBase::SampleResult>``. See
:code-file:`src/core/live_trackers/cpp/inc/live_trackers/schema_tracker.hpp` for
``SchemaTracker`` / ``SampleResult``.
- **Hand-written OpenXR locate tracker** — facade
:code-file:`src/core/deviceio_trackers/cpp/hand_tracker.cpp` (``HandTracker``) and live
backend :code-file:`src/core/live_trackers/cpp/live_hand_tracker_impl.cpp`
(``LiveHandTrackerImpl``): same facade / ``ITrackerImpl`` split, but ``update()`` drives
``xrLocate*`` rather than ``SchemaTracker``.

Step 4: Implement a simple C++ printer (optional)
-------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Table of Contents

references/requirements
references/build
references/generated_trackers
references/retargeting/index
references/camera_streaming
references/mcap_record_replay
Expand Down
165 changes: 165 additions & 0 deletions docs/source/references/generated_trackers.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
.. SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
.. SPDX-License-Identifier: Apache-2.0

Generated Tracker Code
======================

Trackers that only move a FlatBuffer between a plugin and the host — *passthrough* trackers —
are not hand-written. They are declared in a TOML manifest and their C++ and Python glue is
generated when you configure the build. Adding one is a ``.fbs`` schema plus roughly ten lines
of TOML; see :ref:`Passthrough trackers (manifest) <passthrough-tracker-manifest>` for the
authoring workflow.

This page records what is generated today, what is deliberately still hand-written, and what
would have to change to generate the rest.

How it works
------------

Three inputs drive the generator:

- :code-file:`src/core/deviceio_trackers/trackers.toml` — one ``[[tracker]]`` entry per tracker,
carrying only the values that differ from the defaults.
- :code-file:`src/core/deviceio_trackers/defaults.toml` — the defaults, expressed with
``%placeholder%`` substitutions (``%name%``, ``%name_CamelCase%``, and so on).
- :code-dir:`src/core/codegen` — ``manifest.py`` resolves the placeholders, ``templates.py``
derives type and file names, ``templates/`` holds the C++ bodies as ``*.hpp.template`` /
``*.cpp.template`` (``@KEY@`` substitution via ``in_renderer.py``), and ``generate_trackers.py``
renders the output.

:code-file:`cmake/GenerateTrackers.cmake` runs the generator through ``execute_process`` at
**configure** time, not build time, because CMake needs the resulting source list before it can
define targets. Output lands in ``${CMAKE_BINARY_DIR}/generated/trackers/`` and is **not** checked
into git, exactly like the ``flatc`` output. Sources are wired through an explicit
``generated_sources.cmake`` list (not by globbing that directory). Two consequences worth knowing:

- Grepping ``src/`` for a generated class such as ``Se3Tracker`` finds the manifest entry and the
generator, not a ``.cpp``. Searching, debugging, and clangd all need a configured build tree.
- The manifests, every ``.py`` under ``src/core/codegen``, and every template under
``templates/`` are registered as ``CMAKE_CONFIGURE_DEPENDS``, so editing any of them re-runs
configure on the next build.

The generator rewrites a file only when its content changed. Configure also passes
``--prune-stale`` so that after renaming or removing a tracker, orphan headers cannot remain on
the generated include path and keep satisfying a stale ``#include``.

What each entry produces
------------------------

Per manifest entry, in ``${CMAKE_BINARY_DIR}/generated/trackers/``:

.. code-block:: text
:class: code-100col

deviceio_base/<name>_tracker_base.hpp # I<Name>TrackerImpl interface
deviceio_trackers/inc/deviceio_trackers/<name>_tracker.hpp
deviceio_trackers/<name>_tracker.cpp # the ITracker facade
live_trackers/live_<name>_tracker_impl.{hpp,cpp} # wraps SchemaTracker / SchemaPusher
replay_trackers/replay_<name>_tracker_impl.{hpp,cpp}

The shared registration points stay hand-written and ``#include`` a generated ``.inc`` fragment,
so hand-written trackers keep their own rows. The fragments under ``generated/trackers/inc/`` cover
the live and replay factories (includes, forward declarations, try-create thunks, dispatch rows,
factory declarations and definitions), the MCAP recording traits in
:code-file:`src/core/mcap/cpp/inc/mcap/recording_traits.hpp`, the pybind blocks in
:code-file:`src/core/deviceio_trackers/python/tracker_bindings.cpp`, and a
``_generated_tracker_exports.py`` that :code-file:`src/python/isaacteleop/deviceio_trackers/__init__.py`
star-imports. Because that last one is spliced into ``__all__``, a new manifest entry needs no
Python edit at all.

``.pyi`` stubs need no work either: :code-file:`src/core/python/generate_stubs.py` derives them from
the built module.

Generated today
---------------

.. list-table::
:header-rows: 1
:widths: 26 26 48

* - Manifest entry
- Class
- Notes
* - ``joint_state``
- ``JointStateTracker``
- One generic joint-space device (leader arm, exoskeleton, ...)
* - ``se3_tracker``
- ``Se3Tracker``
- Overrides ``class``; the default would give ``Se3TrackerTracker``
* - ``oglo_tactile``
- ``OgloTactileTracker``
- MCAP channels are ``oglo``/``oglo_tracked``, so ``channel`` is overridden
* - ``generic_3axis_pedal``
- ``Generic3AxisPedalTracker``
- Schema lives in ``pedals.fbs``, so ``schema`` and ``channel`` are overridden
* - ``haptic_command``
- ``HapticCommandPushTracker``
- ``direction = "out"``: a typed producer wrapping ``SchemaPusher``

Still hand-written
------------------

.. list-table::
:header-rows: 1
:widths: 34 66

* - Tracker
- Why it is not generated
* - ``HeadTracker``, ``HandTracker``, ``ControllerTracker``
- Real ``xrLocate*`` / hand-tracking calls, not FlatBuffer passthrough
* - ``FullBodyTracker`` (PICO vendor)
- Native ``XR_BD_body_tracking``
* - ``FullBodyTracker`` (Noitom vendor)
- Genuinely a passthrough, but a **vendor** of an existing facade — see below
* - ``MessageChannelTracker``
- ``XR_NV_opaque_data_channel`` with its own connection state machine
* - ``HapticCommandReaderTracker``
- Cross-process consumer: ``read_all_samples`` on one collection, buckets by
``HapticCommand.endpoint`` (left/right). Paired with generated ``HapticCommandPushTracker``.
* - ``FrameMetadataTrackerOak``
- Holds N ``SchemaTracker`` s, one collection per stream — see below
* - ``TensorPushTracker``
- Deliberately kept as the untyped ``bytes`` escape hatch

A quick way to tell the two groups apart: only passthrough impls mention ``SchemaTracker`` or
``SchemaPusher``. Of the hand-written live impls that use those helpers, each is listed above.

Future work
-----------

**Multi-endpoint reader (rejected generated shape).** We briefly generated
``HapticCommandReaderTracker`` with a ``multi_endpoint`` template (bucket samples by
``HapticCommand.endpoint`` on one push-tensor collection). That shape was removed: it made the
codegen tree branchy for a single device, and a generic multi-endpoint reader does not fit
``in/`` or ``out``. The hand-written reader remains the supported approach; a future redesign
(split tensors/collections per endpoint, or a dedicated generator) is optional follow-up.

**Multi-stream shape (OAK).** ``FrameMetadataTrackerOak`` owns one ``SchemaTracker`` per camera
stream, with collection ids derived as ``{prefix}/{StreamName}``. It needs a template variant that
emits an N-collection reader plus per-stream accessors. This was sequenced last during the initial
migration precisely so it could not distort the common template, and it remains the closest
candidate.

**Vendored shape (Noitom).** ``LiveFullBodyTrackerNoitomImpl`` reads
``SchemaTracker<FullBodyPoseRecord, FullBodyPose>``, so it is passthrough by mechanism, but it is a
second *vendor* of the hand-written ``FullBodyTracker`` rather than its own tracker type. Generating
it would need a shape that emits **only** a live impl and a vendor-keyed dispatch row — no facade,
base interface, recording traits, replay half, or pybind block, since it shares all of those with the
PICO vendor. Its ``collection_id`` and ``max_flatbuffer_size`` also arrive through
``TrackerVendor::params`` at runtime instead of being fixed at generation time.

**Schema pybind bindings.** The field-by-field ``src/core/schema/python/*_bindings.h`` files are

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aristarkhovNV, I think you can follow up here for using flatbuffer objs rather than the manually coded bindings as the next step.

still hand-written. They are derivable from the ``.bfbs`` reflection data ``flatc`` already emits
(``--bfbs-gen-embed --reflect-names --reflect-types``), so this is a viable follow-up, but it is a
separate generator with its own risks.

**Manifest ``python_accessor`` (pybind method name).** Generated ``in`` trackers use
``fragments/pybind_in.template``, which binds whatever string ``python_accessor`` holds (default
``get_data``; ``oglo_tactile`` and ``generic_3axis_pedal`` override with device-specific names).
``direction = "out"`` uses ``push`` via defaults. Future work: remove the manifest key and
hard-code ``get_data`` on readers (or maybe ``pull``, paired with ``push`` on producers).
Maybe rename the C++ facade to match if we pick ``pull``, so Python and C++ stay aligned without
per-tracker overrides.

If a prospective shape makes the templates hard to follow, leaving that tracker hand-written is the
correct outcome — the generator exists to remove mechanical duplication, not to model every device.
Loading
Loading