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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ pre-commit install --hook-type commit-msg
```

- **REUSE:** files covered by the REUSE hook need **`SPDX-FileCopyrightText`** and **`SPDX-License-Identifier`** in the form the repo already uses (for example the HTML comment block at the top of `README.md` also applies to **`AGENTS.md`** and similar docs).
- In a filesystem-restricted agent sandbox where `~/.cache/pre-commit` is not writable, set `PRE_COMMIT_HOME` to a writable temporary directory and still run the complete hook set.
- **C++ formatting is enforced by CI, not pre-commit.** The hook set runs `ruff` for Python but does **not** run `clang-format`; CI (`build-ubuntu.yml`) installs **`clang-format-14`** and rejects unformatted C++ as `-Wclang-format-violations`. Before pushing, format touched C++ with the system `clang-format` (match CI's version 14) and verify:

```bash
Expand Down
3 changes: 1 addition & 2 deletions examples/schemaio/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@ cmake_minimum_required(VERSION 3.20)
find_package(Threads REQUIRED)

# Create pusher executable
# Note: Examples link to oxr_core for OpenXR session creation, but pusherio itself doesn't
add_executable(pedal_pusher
pedal_pusher.cpp
)

target_link_libraries(pedal_pusher PRIVATE
Teleop::plugin_utils
pusherio::pusherio
oxr::oxr_core
isaacteleop_schema
${CMAKE_DL_LIBS}
Threads::Threads
Expand Down
42 changes: 19 additions & 23 deletions examples/schemaio/pedal_pusher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
// SPDX-License-Identifier: Apache-2.0

/*!
* @brief Demo application that pushes serialized FlatBuffer Generic3AxisPedalOutput data into the OpenXR runtime.
* @brief Demo application that pushes serialized FlatBuffer Generic3AxisPedalOutput data.
*
* This application demonstrates using the SchemaPusher class to push Generic3AxisPedalOutput FlatBuffer
* messages. The application creates the OpenXR session with required extensions and passes
* the handles to the pusherio library.
* messages. Plugin-facing examples obtain channels from IPluginSession; only the concrete session adapter
* owns transport-specific handles.
*
* Note: Both pusher and reader agree on the schema (Generic3AxisPedalOutput from pedals.fbs), so the schema
* does not need to be sent over the wire.
Expand All @@ -15,7 +15,8 @@
#include "common_utils.hpp"

#include <flatbuffers/flatbuffers.h>
#include <oxr/oxr_session.hpp>
#include <plugin_utils/openxr_plugin_session.hpp>
#include <pusherio/plugin_session.hpp>
#include <pusherio/schema_pusher.hpp>
#include <schema/pedals_generated.h>

Expand All @@ -25,24 +26,19 @@
#include <iostream>
#include <memory>
#include <thread>
#include <utility>

using namespace schemaio_example;

/*!
* @brief Generic3AxisPedalOutput-specific pusher that serializes and pushes foot pedal messages.
*
* Uses composition with SchemaPusher to handle the OpenXR tensor pushing.
* Uses composition with the transport-independent SchemaPusher.
*/
class Generic3AxisPedalPusher
{
public:
Generic3AxisPedalPusher(const core::OpenXRSessionHandles& handles, const std::string& collection_id)
: m_pusher(handles,
core::SchemaPusherConfig{ .collection_id = collection_id,
.max_flatbuffer_size = MAX_FLATBUFFER_SIZE,
.tensor_identifier = "generic_3axis_pedal",
.localized_name = "Generic 3-Axis Pedal Pusher Demo",
.app_name = "Generic3AxisPedalPusher" })
explicit Generic3AxisPedalPusher(std::unique_ptr<core::ISchemaPushChannel> channel) : m_pusher(std::move(channel))
{
}

Expand Down Expand Up @@ -72,21 +68,21 @@ try
{
std::cout << "Schema Pusher (collection: " << COLLECTION_ID << ")" << std::endl;

// Step 1: Create OpenXR session with required extensions for pushing tensor data
std::cout << "[Step 1] Creating OpenXR session with tensor push extensions..." << std::endl;
// Step 1: Select the concrete transport at the composition root.
std::cout << "[Step 1] Creating OpenXR plugin session..." << std::endl;

auto required_extensions = core::SchemaPusher::get_required_extensions();
core::PluginSessionHandle session = std::make_shared<plugin_utils::OpenXRPluginSession>(
"SchemaPusher", core::PluginSessionRequirements{ .schema_push = true });

auto oxr_session = std::make_shared<core::OpenXRSession>("SchemaPusher", required_extensions);

std::cout << " OpenXR session created" << std::endl;

// Step 2: Create the pusher with the session handles
// Step 2: Ask the abstract session for a channel, then give it to the typed pusher.
std::cout << "[Step 2] Creating Generic3AxisPedalPusher..." << std::endl;

std::unique_ptr<Generic3AxisPedalPusher> pusher;
auto handles = oxr_session->get_handles();
pusher = std::make_unique<Generic3AxisPedalPusher>(handles, COLLECTION_ID);
auto pusher = std::make_unique<Generic3AxisPedalPusher>(session->create_schema_push_channel(
core::SchemaPusherConfig{ .collection_id = COLLECTION_ID,
.max_flatbuffer_size = MAX_FLATBUFFER_SIZE,
.tensor_identifier = "generic_3axis_pedal",
.localized_name = "Generic 3-Axis Pedal Pusher Demo",
.app_name = "Generic3AxisPedalPusher" }));

// Step 3: Push samples
std::cout << "[Step 3] Pushing samples..." << std::endl;
Expand Down
4 changes: 4 additions & 0 deletions src/core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ To see **all** `AGENTS.md` files in the IsaacTeleop repo, use the **`find` comma
If work under **`src/core/`** went wrong—**user** correction, **pre-commit/CI** failure, or **repeated** same-class mistakes—you **must** follow the repo root **[`AGENTS.md`](../../AGENTS.md)** **Mandatory learning loop**: distill a short rule and **update** the **nearest** relevant `AGENTS.md` (this file or a package file) or **source comments** in the same session (including **delta vs `main`** scope).

- Async retargeting pacing behavior belongs on the pacing config objects; keep the worker focused on scheduling mechanics and avoid adding concrete pacing-mode or subclass branches there.
- When transport-neutralizing an existing data path, preserve its operation-specific facade and abstract the session-created channel beneath it; add named-port discovery only when a real dynamic-routing requirement needs it.
- Keep session capability declarations transport-neutral in `PluginSessionRequirements`; concrete sessions translate them into backend prerequisites and reject undeclared channel creation.
- Plugin implementations depend on `IPluginSession`, the multiplexed `IPluginPullChannel`, and operation-specific push channels/sources; only composition roots instantiate concrete session adapters, and runtime handles stay inside adapter-owned implementations.
- Reusing OpenXR value structs and enums in a channel contract is acceptable when minimizing migration; keep runtime handles, function pointers, calls, and runtime-clock timestamps inside the OpenXR implementation.
- Prefer coarse-grained async boundaries around an existing synchronous step before splitting DeviceIO/source polling away from graph execution; split internals only when a measured correctness or performance need justifies the extra thread-safety surface.
- In pipelined `TeleopSession`, `last_context` follows the returned completed frame; reset/control-transition events travel with that frame and must not force exact-current-frame waits. Use sync mode for exact current-frame behavior.
- Keep async retargeting comments short and local to invariants; user-facing pacing tuning guidance belongs in docs rather than long code docstrings.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
std::unique_ptr<@IFACE@> LiveDeviceIOFactory::create_@NAME@_tracker_impl(const @CLASS@* tracker)
{
return std::make_unique<@LIVE_IMPL@>(handles_, tracker);
return std::make_unique<@LIVE_IMPL@>(
make_openxr_schema_push_channel(handles_, make_schema_push_config(tracker)));
}
17 changes: 3 additions & 14 deletions src/core/codegen/templates/push/live.cpp.template
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,12 @@

#include <span>
#include <stdexcept>
#include <utility>

namespace core
{
namespace
{
SchemaPusherConfig make_@NAME@_push_config(const @CLASS@* tracker)
{
SchemaPusherConfig cfg;
cfg.collection_id = tracker->collection_id();
cfg.max_flatbuffer_size = tracker->max_payload_size();
cfg.tensor_identifier = tracker->tensor_identifier();
cfg.localized_name = tracker->tensor_identifier();
return cfg;
}
} // namespace
@LIVE_IMPL@::@LIVE_IMPL@(const OpenXRSessionHandles& handles, const @CLASS@* tracker)
: pusher_(handles, make_@NAME@_push_config(tracker))
@LIVE_IMPL@::@LIVE_IMPL@(std::unique_ptr<ISchemaPushChannel> channel)
: pusher_(std::move(channel))
{
// No-op: members set in the initializer list.
}
Expand Down
7 changes: 4 additions & 3 deletions src/core/codegen/templates/push/live.hpp.template
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@

#include <deviceio_base/@BASE_HEADER@.hpp>
#include <deviceio_trackers/@HEADER@.hpp>
#include <oxr_utils/oxr_session_handles.hpp>
#include <pusherio/openxr_schema_push_channel.hpp>
#include <pusherio/schema_pusher.hpp>
#include <schema/@SCHEMA@_generated.h>

#include <cstdint>
#include <memory>
#include <vector>

namespace core
Expand All @@ -19,9 +20,9 @@ class @LIVE_IMPL@ : public @IFACE@
public:
static std::vector<std::string> required_extensions()
{
return SchemaPusher::get_required_extensions();
return OpenXRSchemaPushChannel::get_required_extensions();
}
@LIVE_IMPL@(const OpenXRSessionHandles& handles, const @CLASS@* tracker);
explicit @LIVE_IMPL@(std::unique_ptr<ISchemaPushChannel> channel);
void update(int64_t monotonic_time_ns) override;
void push(const Serialized<@FB_TABLE@>& data) const override;

Expand Down
2 changes: 2 additions & 0 deletions src/core/live_trackers/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ SPDX-License-Identifier: Apache-2.0

## Time and OpenXR

- Keep push-channel selection in `LiveDeviceIOFactory`; push tracker impls receive an
`ISchemaPushChannel` and do not construct OpenXR channels from session handles.
- Store **`last_update_time_` as `int64_t`** (monotonic ns), not **`XrTime`**.
- **Once per `update` call:** `const XrTime xr_time = time_converter_.convert_monotonic_ns_to_xrtime(monotonic_time_ns);` then use **`xr_time`** for every **`xrLocate*`** / hand / body call **and** for MCAP (see below). **Do not** call **`convert_monotonic_ns_to_xrtime`** again in the MCAP block.
- **Full-body limp mode:** if the body tracker handle is null and you **return early**, **do not** compute **`xr_time`** first—only convert after you know you will call OpenXR.
Expand Down
13 changes: 12 additions & 1 deletion src/core/live_trackers/cpp/live_deviceio_factory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <deviceio_trackers/message_channel_tracker.hpp>
#include <deviceio_trackers/tensor_push_tracker.hpp>
#include <oxr_utils/oxr_time.hpp>
#include <pusherio/openxr_schema_push_channel.hpp>

#include <cassert>
#include <optional>
Expand Down Expand Up @@ -56,6 +57,15 @@ bool try_add_extensions(const ITracker& tracker, std::set<std::string>& out)
return true;
}

template <typename TrackerT>
SchemaPusherConfig make_schema_push_config(const TrackerT* tracker)
{
return SchemaPusherConfig{ .collection_id = tracker->collection_id(),
.max_flatbuffer_size = tracker->max_payload_size(),
.tensor_identifier = tracker->tensor_identifier(),
.localized_name = tracker->tensor_identifier() };
}

std::unique_ptr<ITrackerImpl> try_create_head_impl(LiveDeviceIOFactory& factory, const ITracker& tracker)
{
auto* typed = dynamic_cast<const HeadTracker*>(&tracker);
Expand Down Expand Up @@ -455,7 +465,8 @@ std::unique_ptr<IFullBodyTrackerImpl> LiveDeviceIOFactory::create_full_body_trac

std::unique_ptr<ITensorPushTrackerImpl> LiveDeviceIOFactory::create_tensor_push_tracker_impl(const TensorPushTracker* tracker)
{
return std::make_unique<LiveTensorPushTrackerImpl>(handles_, tracker);
return std::make_unique<LiveTensorPushTrackerImpl>(
make_openxr_schema_push_channel(handles_, make_schema_push_config(tracker)));
}

std::unique_ptr<IHapticCommandReaderTrackerImpl> LiveDeviceIOFactory::create_haptic_command_reader_tracker_impl(
Expand Down
21 changes: 4 additions & 17 deletions src/core/live_trackers/cpp/live_tensor_push_tracker_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,13 @@

#include <oxr_utils/os_time.hpp>

namespace core
{

namespace
{
#include <utility>

SchemaPusherConfig make_tensor_push_config(const TensorPushTracker* tracker)
namespace core
{
SchemaPusherConfig cfg;
cfg.collection_id = tracker->collection_id();
cfg.max_flatbuffer_size = tracker->max_payload_size();
cfg.tensor_identifier = tracker->tensor_identifier();
cfg.localized_name = tracker->tensor_identifier();
return cfg;
}

} // namespace

LiveTensorPushTrackerImpl::LiveTensorPushTrackerImpl(const OpenXRSessionHandles& handles, const TensorPushTracker* tracker)
: pusher_(handles, make_tensor_push_config(tracker))
LiveTensorPushTrackerImpl::LiveTensorPushTrackerImpl(std::unique_ptr<ISchemaPushChannel> channel)
: pusher_(std::move(channel))
{
}

Expand Down
10 changes: 5 additions & 5 deletions src/core/live_trackers/cpp/live_tensor_push_tracker_impl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,26 @@
#pragma once

#include <deviceio_base/tensor_push_tracker_base.hpp>
#include <deviceio_trackers/tensor_push_tracker.hpp>
#include <oxr_utils/oxr_session_handles.hpp>
#include <pusherio/openxr_schema_push_channel.hpp>
#include <pusherio/schema_pusher.hpp>

#include <cstdint>
#include <memory>
#include <vector>

namespace core
{

// Wraps core::SchemaPusher; owns the XR_NVX1_push_tensor handle.
// Wraps core::SchemaPusher and owns its transport channel.
class LiveTensorPushTrackerImpl : public ITensorPushTrackerImpl
{
public:
static std::vector<std::string> required_extensions()
{
return SchemaPusher::get_required_extensions();
return OpenXRSchemaPushChannel::get_required_extensions();
}

LiveTensorPushTrackerImpl(const OpenXRSessionHandles& handles, const TensorPushTracker* tracker);
explicit LiveTensorPushTrackerImpl(std::unique_ptr<ISchemaPushChannel> channel);

LiveTensorPushTrackerImpl(const LiveTensorPushTrackerImpl&) = delete;
LiveTensorPushTrackerImpl& operator=(const LiveTensorPushTrackerImpl&) = delete;
Expand Down
9 changes: 9 additions & 0 deletions src/core/pusherio/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ cmake_minimum_required(VERSION 3.20)

# PusherIO library (static; consumed in-tree via pusherio::pusherio)
add_library(pusherio STATIC
hand_tracking_pusher.cpp
openxr_schema_push_channel.cpp
inc/pusherio/hand_tracking_push_channel.hpp
inc/pusherio/hand_tracking_pusher.hpp
inc/pusherio/openxr_schema_push_channel.hpp
inc/pusherio/plugin_session.hpp
inc/pusherio/wrist_tracking_source.hpp
schema_pusher.cpp
inc/pusherio/schema_pusher.hpp
)
Expand All @@ -16,6 +23,8 @@ target_include_directories(pusherio

target_link_libraries(pusherio
PUBLIC
deviceio::deviceio_base

# oxr_utils is header-only and provides OpenXRSessionHandles struct
# It also brings in OpenXR headers for base types without linking to OpenXR loader
oxr::oxr_utils
Expand Down
32 changes: 32 additions & 0 deletions src/core/pusherio/cpp/hand_tracking_pusher.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

#include "inc/pusherio/hand_tracking_pusher.hpp"

#include <stdexcept>
#include <utility>

namespace core
{

HandTrackingPusher::HandTrackingPusher(std::unique_ptr<IHandTrackingPushChannel> channel) : channel_(std::move(channel))
{
if (!channel_)
{
throw std::invalid_argument("HandTrackingPusher requires a channel");
}
}

HandTrackingPusher::~HandTrackingPusher() = default;

void HandTrackingPusher::push(const XrHandJointLocationEXT* joint_locations, int64_t sample_time_local_common_clock_ns)
{
if (!joint_locations)
{
throw std::invalid_argument("HandTrackingPusher requires joint locations");
}

channel_->push(joint_locations, sample_time_local_common_clock_ns);
}

} // namespace core
32 changes: 32 additions & 0 deletions src/core/pusherio/cpp/inc/pusherio/hand_tracking_push_channel.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

#pragma once

#include <openxr/openxr.h>

#include <cstdint>

namespace core
{

/*!
* @brief Transport-owned channel for publishing one hand's tracking data.
*
* OpenXR value types define the established hand data shape. Runtime handles,
* function pointers, and calls remain private to concrete channel implementations.
*/
class IHandTrackingPushChannel
{
public:
// Orderly destruction closes the logical hand stream and makes it inactive
// at the receiver. Remote implementations must also expire active state
// after an unexpected transport disconnect.
virtual ~IHandTrackingPushChannel() = default;

// joint_locations contains XR_HAND_JOINT_COUNT_EXT entries and is borrowed
// for this call only. Asynchronous transports must copy it before returning.
virtual void push(const XrHandJointLocationEXT* joint_locations, int64_t sample_time_local_common_clock_ns) = 0;
};

} // namespace core
Loading
Loading