diff --git a/docs/source/device/add_device.rst b/docs/source/device/add_device.rst index 3b98b270b..a36f0ead3 100644 --- a/docs/source/device/add_device.rst +++ b/docs/source/device/add_device.rst @@ -117,7 +117,7 @@ tensor samples from OpenXR. Implement a concrete tracker class (e.g. - **Factory registration** — Register your tracker in the live factory dispatch table (see ``LiveDeviceIOFactory``). The factory constructs an ``ITrackerImpl`` that holds a ``SchemaTracker``, builds a ``SchemaTrackerConfig`` from the tracker's stored - configuration, and implements ``update(XrTime)`` and + configuration, and implements ``update(int64_t monotonic_time_ns)`` and ``serialize_all(channel_index, callback)``. In the **Impl**: diff --git a/examples/lerobot/record.py b/examples/lerobot/record.py index 8dace6ec3..6b7aa973d 100644 --- a/examples/lerobot/record.py +++ b/examples/lerobot/record.py @@ -110,7 +110,7 @@ def main(): try: while time.time() - start_time < 10.0: # Update session and all trackers - session.update() + session.update(time.monotonic_ns()) # Get hand data left_tracked: schema.HandPoseTrackedT = hand_tracker.get_left_hand( diff --git a/examples/oxr/cpp/oxr_session_sharing.cpp b/examples/oxr/cpp/oxr_session_sharing.cpp index fdb33ef92..617e73a5b 100644 --- a/examples/oxr/cpp/oxr_session_sharing.cpp +++ b/examples/oxr/cpp/oxr_session_sharing.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -76,8 +77,9 @@ try for (int i = 0; i < 10; ++i) { // Both sessions update using the same underlying OpenXR session - session1->update(); - session2->update(); + const int64_t graph_time_ns = core::os_monotonic_now_ns(); + session1->update(graph_time_ns); + session2->update(graph_time_ns); // Get data from both trackers const auto& left_tracked = hand_tracker->get_left_hand(*session1); diff --git a/examples/oxr/cpp/oxr_simple_api_demo.cpp b/examples/oxr/cpp/oxr_simple_api_demo.cpp index 15d0176bb..89247a8cb 100644 --- a/examples/oxr/cpp/oxr_simple_api_demo.cpp +++ b/examples/oxr/cpp/oxr_simple_api_demo.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -76,7 +77,7 @@ try for (int i = 0; i < 5; ++i) { // Session handles internal update() calls to trackers - session->update(); + session->update(core::os_monotonic_now_ns()); // External user only uses public query methods const auto& left_tracked = hand_tracker->get_left_hand(*session); diff --git a/examples/oxr/python/modular_example.py b/examples/oxr/python/modular_example.py index fa1f7b6a2..f21eb78a2 100755 --- a/examples/oxr/python/modular_example.py +++ b/examples/oxr/python/modular_example.py @@ -60,7 +60,7 @@ def main(): while time.time() - start_time < 10.0: # Update session and all trackers - session.update() + session.update(time.monotonic_ns()) # Print every 60 frames (~1 second) if frame_count % 60 == 0: diff --git a/examples/oxr/python/modular_example_with_mcap.py b/examples/oxr/python/modular_example_with_mcap.py index 65ce8b8ab..c995843fb 100644 --- a/examples/oxr/python/modular_example_with_mcap.py +++ b/examples/oxr/python/modular_example_with_mcap.py @@ -71,7 +71,7 @@ def main(): start_time = time.time() while time.time() - start_time < 30.0: - session.update() + session.update(time.monotonic_ns()) # Print every 60 frames (~1 second) if frame_count % 60 == 0: diff --git a/examples/oxr/python/test_controller_tracker.py b/examples/oxr/python/test_controller_tracker.py index a0cc3c1d8..a14c93662 100644 --- a/examples/oxr/python/test_controller_tracker.py +++ b/examples/oxr/python/test_controller_tracker.py @@ -51,7 +51,7 @@ # Test 4: Initial update print("[Test 4] Testing initial data retrieval...") - session.update() + session.update(time.monotonic_ns()) print("✓ Update successful") print() @@ -101,7 +101,7 @@ def assert_trackers_consistent(label, ta, tb): last_status_print = start_time while time.time() - start_time < 10.0: - session.update() + session.update(time.monotonic_ns()) current_time = time.time() if current_time - last_status_print >= 0.5: diff --git a/examples/oxr/python/test_extensions.py b/examples/oxr/python/test_extensions.py index 869cbe183..722d57734 100755 --- a/examples/oxr/python/test_extensions.py +++ b/examples/oxr/python/test_extensions.py @@ -9,6 +9,8 @@ do not expose get_required_extensions(). """ +import time + import isaacteleop.deviceio as deviceio import isaacteleop.oxr as oxr @@ -87,7 +89,7 @@ print(" ✅ Initialized successfully") # Quick update test - session.update() + session.update(time.monotonic_ns()) left_tracked = hand.get_left_hand(session) head_tracked = head.get_head(session) print(" ✅ Update successful") diff --git a/examples/oxr/python/test_full_body_tracker.py b/examples/oxr/python/test_full_body_tracker.py index 06a8b2073..45d095f60 100644 --- a/examples/oxr/python/test_full_body_tracker.py +++ b/examples/oxr/python/test_full_body_tracker.py @@ -55,7 +55,7 @@ # Test 5: Initial update print("[Test 5] Testing initial data retrieval...") - session.update() + session.update(time.monotonic_ns()) print("✓ Update successful") print() @@ -86,7 +86,7 @@ last_status_print = start_time while time.time() - start_time < 10.0: - session.update() + session.update(time.monotonic_ns()) # Get current body pose current_time = time.time() diff --git a/examples/oxr/python/test_hand_inactive_on_plugin_stop.py b/examples/oxr/python/test_hand_inactive_on_plugin_stop.py index 63c90ffb2..9820d254b 100644 --- a/examples/oxr/python/test_hand_inactive_on_plugin_stop.py +++ b/examples/oxr/python/test_hand_inactive_on_plugin_stop.py @@ -29,7 +29,7 @@ def poll_hands(hand_tracker, deviceio_session): """Return (left_active, right_active) for the current frame.""" - deviceio_session.update() + deviceio_session.update(time.monotonic_ns()) left = hand_tracker.get_left_hand(deviceio_session) right = hand_tracker.get_right_hand(deviceio_session) return left.data is not None, right.data is not None diff --git a/examples/oxr/python/test_modular.py b/examples/oxr/python/test_modular.py index c1f1cd5e0..e519da7f6 100755 --- a/examples/oxr/python/test_modular.py +++ b/examples/oxr/python/test_modular.py @@ -46,7 +46,7 @@ # Test 4: Update and get data print("[Test 4] Testing data retrieval...") - session.update() + session.update(time.monotonic_ns()) print("✓ Update successful") print() @@ -89,7 +89,7 @@ start_time = time.time() while time.time() - start_time < 5.0: - session.update() + session.update(time.monotonic_ns()) if frame_count % 60 == 0: elapsed = time.time() - start_time diff --git a/examples/oxr/python/test_oak_camera.py b/examples/oxr/python/test_oak_camera.py index a48129950..5395061fe 100755 --- a/examples/oxr/python/test_oak_camera.py +++ b/examples/oxr/python/test_oak_camera.py @@ -84,7 +84,7 @@ def _run_schema_pusher( while time.time() - start_time < duration: plugin.check_health() - session.update() + session.update(time.monotonic_ns()) frame_count += 1 elapsed = time.time() - start_time diff --git a/examples/oxr/python/test_session_sharing.py b/examples/oxr/python/test_session_sharing.py index 2eba6c65c..d3983d148 100755 --- a/examples/oxr/python/test_session_sharing.py +++ b/examples/oxr/python/test_session_sharing.py @@ -83,8 +83,8 @@ frame_count = 0 while time.time() - start_time < 5.0: # Both sessions update using the same underlying OpenXR session - session1.update() - session2.update() + session1.update(time.monotonic_ns()) + session2.update(time.monotonic_ns()) # Print status every 60 frames if frame_count % 60 == 0: diff --git a/examples/oxr/python/test_synthetic_hands.py b/examples/oxr/python/test_synthetic_hands.py index 5b626d9d0..bf02551f0 100644 --- a/examples/oxr/python/test_synthetic_hands.py +++ b/examples/oxr/python/test_synthetic_hands.py @@ -96,7 +96,7 @@ def run_test(): if frame_count % 60 == 0: plugin.check_health() # Throws PluginCrashException if plugin crashed - deviceio_session.update() + deviceio_session.update(time.monotonic_ns()) if frame_count % 60 == 0: left_tracked = hand_tracker.get_left_hand(deviceio_session) diff --git a/examples/retargeting/python/sources_example.py b/examples/retargeting/python/sources_example.py index 602db91d8..d629ae5c3 100755 --- a/examples/retargeting/python/sources_example.py +++ b/examples/retargeting/python/sources_example.py @@ -121,7 +121,7 @@ def main(): while time.time() - start_time < 10.0: # Update session and all trackers - session.update() + session.update(time.monotonic_ns()) # Print every 60 frames (~1 second) if frame_count % 60 == 0: diff --git a/examples/schemaio/frame_metadata_printer.cpp b/examples/schemaio/frame_metadata_printer.cpp index b7ce5ac2e..dd41494b1 100644 --- a/examples/schemaio/frame_metadata_printer.cpp +++ b/examples/schemaio/frame_metadata_printer.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -107,7 +108,7 @@ try while (true) { - session->update(); + session->update(core::os_monotonic_now_ns()); // Refresh stream count and extend per-stream tracking if streams were added. stream_count = tracker->get_stream_count(); diff --git a/examples/schemaio/pedal_printer.cpp b/examples/schemaio/pedal_printer.cpp index e00740d0a..6f934ad89 100644 --- a/examples/schemaio/pedal_printer.cpp +++ b/examples/schemaio/pedal_printer.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -69,7 +70,7 @@ try while (received_count < MAX_SAMPLES) { // Update session (this calls update on all trackers) - session->update(); + session->update(core::os_monotonic_now_ns()); // Print current data if available const auto& tracked = tracker->get_data(*session); diff --git a/src/core/deviceio_base/cpp/CMakeLists.txt b/src/core/deviceio_base/cpp/CMakeLists.txt index 2defe9713..c01d0ece5 100644 --- a/src/core/deviceio_base/cpp/CMakeLists.txt +++ b/src/core/deviceio_base/cpp/CMakeLists.txt @@ -14,8 +14,6 @@ target_include_directories(deviceio_base target_link_libraries(deviceio_base INTERFACE isaacteleop_schema - oxr::oxr_utils - OpenXR::headers ) add_library(deviceio::deviceio_base ALIAS deviceio_base) diff --git a/src/core/deviceio_base/cpp/inc/deviceio_base/tracker.hpp b/src/core/deviceio_base/cpp/inc/deviceio_base/tracker.hpp index 4a28cb671..90ff245df 100644 --- a/src/core/deviceio_base/cpp/inc/deviceio_base/tracker.hpp +++ b/src/core/deviceio_base/cpp/inc/deviceio_base/tracker.hpp @@ -3,8 +3,7 @@ #pragma once -#include - +#include #include #include #include @@ -12,9 +11,6 @@ namespace core { -// Forward declarations -struct OpenXRSessionHandles; - // Base interface for tracker implementations. // The actual worker objects updated each frame by DeviceIOSession. class ITrackerImpl @@ -23,13 +19,18 @@ class ITrackerImpl virtual ~ITrackerImpl() = default; /** - * @brief Updates tracker state for the specified OpenXR time. + * @brief Updates tracker state for the current graph step. + * + * @param graph_time_ns Logical timestamp for this update step (nanoseconds, + * monotonic clock). Used as the MCAP logTime/publishTime. Tracker + * implementations that perform synchronous device queries compute + * their own wall-clock "now" for DeviceDataTimestamp fields. * * @throws std::runtime_error On critical tracker/runtime failures. * @note A thrown exception indicates a fatal condition; the application is * expected to terminate rather than continue running. */ - virtual void update(XrTime time) = 0; + virtual void update(int64_t graph_time_ns) = 0; }; /** diff --git a/src/core/deviceio_session/cpp/CMakeLists.txt b/src/core/deviceio_session/cpp/CMakeLists.txt index 31ecf2a04..37c7972e3 100644 --- a/src/core/deviceio_session/cpp/CMakeLists.txt +++ b/src/core/deviceio_session/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ target_include_directories(deviceio_session target_link_libraries(deviceio_session PUBLIC deviceio::deviceio_base + oxr::oxr_utils Teleop::openxr_extensions PRIVATE deviceio::deviceio_trackers diff --git a/src/core/deviceio_session/cpp/deviceio_session.cpp b/src/core/deviceio_session/cpp/deviceio_session.cpp index 72e7549b8..755607ac9 100644 --- a/src/core/deviceio_session/cpp/deviceio_session.cpp +++ b/src/core/deviceio_session/cpp/deviceio_session.cpp @@ -24,7 +24,7 @@ namespace core DeviceIOSession::DeviceIOSession(const std::vector>& trackers, const OpenXRSessionHandles& handles, std::optional recording_config) - : handles_(handles), time_converter_(handles) + : handles_(handles) { std::vector> tracker_names; @@ -101,13 +101,11 @@ std::unique_ptr DeviceIOSession::run(const std::vector(new DeviceIOSession(trackers, handles, std::move(recording_config))); } -void DeviceIOSession::update() +void DeviceIOSession::update(int64_t graph_time_ns) { - XrTime current_time = time_converter_.os_monotonic_now(); - for (auto& kv : tracker_impls_) { - kv.second->update(current_time); + kv.second->update(graph_time_ns); } } diff --git a/src/core/deviceio_session/cpp/inc/deviceio_session/deviceio_session.hpp b/src/core/deviceio_session/cpp/inc/deviceio_session/deviceio_session.hpp index 09965c8d7..765236474 100644 --- a/src/core/deviceio_session/cpp/inc/deviceio_session/deviceio_session.hpp +++ b/src/core/deviceio_session/cpp/inc/deviceio_session/deviceio_session.hpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include @@ -65,11 +64,14 @@ class DeviceIOSession : public ITrackerSession * If recording is active, tracker implementations write MCAP samples * directly during this call. * + * @param graph_time_ns Logical timestamp for this update step (nanoseconds, + * monotonic clock). Forwarded to tracker impls for MCAP envelope time. + * * @throws std::runtime_error On critical tracker/runtime failures. * @note A thrown exception indicates a fatal condition; the application is * expected to terminate rather than continue running. */ - void update(); + void update(int64_t graph_time_ns); const ITrackerImpl& get_tracker_impl(const ITracker& tracker) const override { @@ -88,7 +90,6 @@ class DeviceIOSession : public ITrackerSession const OpenXRSessionHandles handles_; std::unordered_map> tracker_impls_; - XrTimeConverter time_converter_; // Owned MCAP writer; null when recording is not configured. std::unique_ptr mcap_writer_; diff --git a/src/core/deviceio_session/py_utils/inc/deviceio_py_utils/session.hpp b/src/core/deviceio_session/py_utils/inc/deviceio_py_utils/session.hpp index 001919b6f..b60a5393b 100644 --- a/src/core/deviceio_session/py_utils/inc/deviceio_py_utils/session.hpp +++ b/src/core/deviceio_session/py_utils/inc/deviceio_py_utils/session.hpp @@ -29,13 +29,13 @@ class PyDeviceIOSession : public ITrackerSession { } - void update() + void update(int64_t graph_time_ns) { if (!impl_) { throw std::runtime_error("Session has been closed/destroyed"); } - impl_->update(); + impl_->update(graph_time_ns); } void close() diff --git a/src/core/deviceio_session/python/session_bindings.cpp b/src/core/deviceio_session/python/session_bindings.cpp index f640edbd1..ce09a0cf1 100644 --- a/src/core/deviceio_session/python/session_bindings.cpp +++ b/src/core/deviceio_session/python/session_bindings.cpp @@ -41,7 +41,8 @@ PYBIND11_MODULE(_deviceio_session, m) py::class_>( m, "DeviceIOSession") - .def("update", &core::PyDeviceIOSession::update, "Update session and all trackers") + .def("update", &core::PyDeviceIOSession::update, py::arg("graph_time_ns"), + "Update session and all trackers. graph_time_ns is used as MCAP logTime/publishTime.") .def("close", &core::PyDeviceIOSession::close, "Release the native session immediately (usually automatic via context manager)") .def("__enter__", &core::PyDeviceIOSession::enter) diff --git a/src/core/deviceio_trackers/cpp/CMakeLists.txt b/src/core/deviceio_trackers/cpp/CMakeLists.txt index abfafb147..0ea801774 100644 --- a/src/core/deviceio_trackers/cpp/CMakeLists.txt +++ b/src/core/deviceio_trackers/cpp/CMakeLists.txt @@ -27,8 +27,6 @@ target_include_directories(deviceio_trackers target_link_libraries(deviceio_trackers PUBLIC deviceio::deviceio_base - PRIVATE - Teleop::openxr_extensions ) add_library(deviceio::deviceio_trackers ALIAS deviceio_trackers) diff --git a/src/core/deviceio_trackers/cpp/hand_tracker.cpp b/src/core/deviceio_trackers/cpp/hand_tracker.cpp index 514fcdb85..f5c8d6e8d 100644 --- a/src/core/deviceio_trackers/cpp/hand_tracker.cpp +++ b/src/core/deviceio_trackers/cpp/hand_tracker.cpp @@ -3,8 +3,6 @@ #include "inc/deviceio_trackers/hand_tracker.hpp" -#include - namespace core { @@ -22,41 +20,4 @@ const HandPoseTrackedT& HandTracker::get_right_hand(const ITrackerSession& sessi return static_cast(session.get_tracker_impl(*this)).get_right_hand(); } -std::string HandTracker::get_joint_name(uint32_t joint_index) -{ - static constexpr std::array joint_names = { { "Palm", - "Wrist", - "Thumb_Metacarpal", - "Thumb_Proximal", - "Thumb_Distal", - "Thumb_Tip", - "Index_Metacarpal", - "Index_Proximal", - "Index_Intermediate", - "Index_Distal", - "Index_Tip", - "Middle_Metacarpal", - "Middle_Proximal", - "Middle_Intermediate", - "Middle_Distal", - "Middle_Tip", - "Ring_Metacarpal", - "Ring_Proximal", - "Ring_Intermediate", - "Ring_Distal", - "Ring_Tip", - "Little_Metacarpal", - "Little_Proximal", - "Little_Intermediate", - "Little_Distal", - "Little_Tip" } }; - static_assert(joint_names.size() == XR_HAND_JOINT_COUNT_EXT, "joint names count must match XR_HAND_JOINT_COUNT_EXT"); - - if (joint_index < joint_names.size()) - { - return joint_names[joint_index]; - } - return "Unknown"; -} - } // namespace core diff --git a/src/core/deviceio_trackers/cpp/inc/deviceio_trackers/hand_tracker.hpp b/src/core/deviceio_trackers/cpp/inc/deviceio_trackers/hand_tracker.hpp index d45a59c4a..f68d76308 100644 --- a/src/core/deviceio_trackers/cpp/inc/deviceio_trackers/hand_tracker.hpp +++ b/src/core/deviceio_trackers/cpp/inc/deviceio_trackers/hand_tracker.hpp @@ -6,8 +6,6 @@ #include #include -#include - namespace core { @@ -26,9 +24,6 @@ class HandTracker : public ITracker const HandPoseTrackedT& get_left_hand(const ITrackerSession& session) const; const HandPoseTrackedT& get_right_hand(const ITrackerSession& session) const; - /** @brief Get joint name for debugging. */ - static std::string get_joint_name(uint32_t joint_index); - private: static constexpr const char* TRACKER_NAME = "HandTracker"; }; diff --git a/src/core/deviceio_trackers/python/tracker_bindings.cpp b/src/core/deviceio_trackers/python/tracker_bindings.cpp index 0dd83d63a..bd6f75d05 100644 --- a/src/core/deviceio_trackers/python/tracker_bindings.cpp +++ b/src/core/deviceio_trackers/python/tracker_bindings.cpp @@ -7,9 +7,9 @@ #include #include #include -#include #include #include +#include namespace py = pybind11; @@ -35,8 +35,7 @@ PYBIND11_MODULE(_deviceio_trackers, m) "get_right_hand", [](const core::HandTracker& self, const core::ITrackerSession& session) -> core::HandPoseTrackedT { return self.get_right_hand(session); }, - py::arg("session")) - .def_static("get_joint_name", &core::HandTracker::get_joint_name); + py::arg("session")); py::class_>(m, "HeadTracker") .def(py::init<>()) @@ -96,9 +95,24 @@ PYBIND11_MODULE(_deviceio_trackers, m) { return self.get_body_pose(session); }, py::arg("session"), "Get full body pose tracked state (data is None if inactive)"); - m.attr("NUM_JOINTS") = static_cast(XR_HAND_JOINT_COUNT_EXT); - m.attr("JOINT_PALM") = static_cast(XR_HAND_JOINT_PALM_EXT); - m.attr("JOINT_WRIST") = static_cast(XR_HAND_JOINT_WRIST_EXT); - m.attr("JOINT_THUMB_TIP") = static_cast(XR_HAND_JOINT_THUMB_TIP_EXT); - m.attr("JOINT_INDEX_TIP") = static_cast(XR_HAND_JOINT_INDEX_TIP_EXT); +<<<<<<< HEAD + m.attr("NUM_JOINTS") = static_cast(core::HandJoint_NUM_JOINTS); + m.attr("JOINT_PALM") = static_cast(core::HandJoint_PALM); + m.attr("JOINT_WRIST") = static_cast(core::HandJoint_WRIST); + m.attr("JOINT_THUMB_TIP") = static_cast(core::HandJoint_THUMB_TIP); + m.attr("JOINT_INDEX_TIP") = static_cast(core::HandJoint_INDEX_TIP); +======= + // Hand joint indices from the OpenXR XR_EXT_hand_tracking extension (v1.0). + // Re-exported to Python via deviceio_trackers_init.py / deviceio_init.py. + // XR_HAND_JOINT_PALM_EXT = 0 + // XR_HAND_JOINT_WRIST_EXT = 1 + // XR_HAND_JOINT_THUMB_TIP_EXT = 5 + // XR_HAND_JOINT_INDEX_TIP_EXT = 10 + // XR_HAND_JOINT_COUNT_EXT = 26 + m.attr("NUM_JOINTS") = 26; + m.attr("JOINT_PALM") = 0; + m.attr("JOINT_WRIST") = 1; + m.attr("JOINT_THUMB_TIP") = 5; + m.attr("JOINT_INDEX_TIP") = 10; +>>>>>>> d1504818 (Update timestamps to monotonic, and use Update timestamp for the mcap timestamp.) } diff --git a/src/core/live_trackers/cpp/CMakeLists.txt b/src/core/live_trackers/cpp/CMakeLists.txt index f562f718d..a2e429215 100644 --- a/src/core/live_trackers/cpp/CMakeLists.txt +++ b/src/core/live_trackers/cpp/CMakeLists.txt @@ -33,6 +33,7 @@ target_link_libraries(live_trackers deviceio::deviceio_base deviceio::deviceio_trackers mcap::mcap_core + oxr::oxr_utils Teleop::openxr_extensions ) diff --git a/src/core/live_trackers/cpp/inc/live_trackers/schema_tracker.hpp b/src/core/live_trackers/cpp/inc/live_trackers/schema_tracker.hpp index 81143e01b..0210e8a06 100644 --- a/src/core/live_trackers/cpp/inc/live_trackers/schema_tracker.hpp +++ b/src/core/live_trackers/cpp/inc/live_trackers/schema_tracker.hpp @@ -52,6 +52,9 @@ class SchemaTracker : public SchemaTrackerBase * and written to the MCAP channel. The last sample's unpacked data is * returned via out_latest (if non-null and samples were read). * + * @param log_time_ns Monotonic timestamp (nanoseconds) used as the MCAP + * envelope logTime/publishTime for all samples written in this call. + * Typically the value passed to ITrackerImpl::update(). * @param out_latest If non-null and samples were read, receives the unpacked * data from the last sample. Cleared when the tensor collection * is absent. @@ -60,7 +63,7 @@ class SchemaTracker : public SchemaTrackerBase * @note Missing collection, temporary collection loss, and "no new sample" * are treated as common non-fatal conditions and do not throw. */ - void update(std::shared_ptr& out_latest) + void update(int64_t log_time_ns, std::shared_ptr& out_latest) { samples_.clear(); bool present = read_all_samples(samples_); @@ -88,11 +91,9 @@ class SchemaTracker : public SchemaTrackerBase } fb->UnPackTo(out_latest.get()); - // write() serializes synchronously and does not retain the shared_ptr, - // so reusing out_latest across loop iterations is safe. if (mcap_channels_) { - mcap_channels_->write(mcap_channel_index_, sample.timestamp, out_latest); + mcap_channels_->write(mcap_channel_index_, log_time_ns, sample.timestamp, out_latest); } } } diff --git a/src/core/live_trackers/cpp/live_controller_tracker_impl.cpp b/src/core/live_trackers/cpp/live_controller_tracker_impl.cpp index b6464a8a9..8e20a99d7 100644 --- a/src/core/live_trackers/cpp/live_controller_tracker_impl.cpp +++ b/src/core/live_trackers/cpp/live_controller_tracker_impl.cpp @@ -314,9 +314,10 @@ LiveControllerTrackerImpl::LiveControllerTrackerImpl(const OpenXRSessionHandles& std::cout << "ControllerTracker initialized (left + right) with action context" << std::endl; } -void LiveControllerTrackerImpl::update(XrTime time) +void LiveControllerTrackerImpl::update(int64_t graph_time_ns) { - last_update_time_ = time; + int64_t now_ns = os_monotonic_now_ns(); + XrTime time = time_converter_.convert_monotonic_ns_to_xrtime(now_ns); // Sync actions via xrSyncActions2NV with our session action context XrActiveActionSet active_action_set{ action_set_.get(), XR_NULL_PATH }; @@ -419,10 +420,11 @@ void LiveControllerTrackerImpl::update(XrTime time) if (mcap_channels_) { - int64_t monotonic_ns = time_converter_.convert_xrtime_to_monotonic_ns(last_update_time_); - DeviceDataTimestamp timestamp(monotonic_ns, monotonic_ns, last_update_time_); - mcap_channels_->write(0, timestamp, left_tracked_.data); - mcap_channels_->write(1, timestamp, right_tracked_.data); + // TODO: Replace with actual client-reported sample/available timestamps + // once the runtime exposes per-sample timing metadata. + DeviceDataTimestamp timestamp(now_ns, now_ns, static_cast(time)); + mcap_channels_->write(0, graph_time_ns, timestamp, left_tracked_.data); + mcap_channels_->write(1, graph_time_ns, timestamp, right_tracked_.data); } } diff --git a/src/core/live_trackers/cpp/live_controller_tracker_impl.hpp b/src/core/live_trackers/cpp/live_controller_tracker_impl.hpp index 66ac5c06f..99c8c5aca 100644 --- a/src/core/live_trackers/cpp/live_controller_tracker_impl.hpp +++ b/src/core/live_trackers/cpp/live_controller_tracker_impl.hpp @@ -40,7 +40,7 @@ class LiveControllerTrackerImpl : public IControllerTrackerImpl LiveControllerTrackerImpl(LiveControllerTrackerImpl&&) = delete; LiveControllerTrackerImpl& operator=(LiveControllerTrackerImpl&&) = delete; - void update(XrTime time) override; + void update(int64_t graph_time_ns) override; const ControllerSnapshotTrackedT& get_left_controller() const override; const ControllerSnapshotTrackedT& get_right_controller() const override; @@ -75,7 +75,6 @@ class LiveControllerTrackerImpl : public IControllerTrackerImpl ControllerSnapshotTrackedT left_tracked_; ControllerSnapshotTrackedT right_tracked_; - XrTime last_update_time_ = 0; std::unique_ptr mcap_channels_; }; diff --git a/src/core/live_trackers/cpp/live_frame_metadata_tracker_oak_impl.cpp b/src/core/live_trackers/cpp/live_frame_metadata_tracker_oak_impl.cpp index a8bc77cb0..c35c44118 100644 --- a/src/core/live_trackers/cpp/live_frame_metadata_tracker_oak_impl.cpp +++ b/src/core/live_trackers/cpp/live_frame_metadata_tracker_oak_impl.cpp @@ -59,13 +59,11 @@ LiveFrameMetadataTrackerOakImpl::LiveFrameMetadataTrackerOakImpl(const OpenXRSes } } -void LiveFrameMetadataTrackerOakImpl::update(XrTime /*time*/) +void LiveFrameMetadataTrackerOakImpl::update(int64_t graph_time_ns) { - // Policy: per-stream SchemaTracker throws on critical OpenXR/tensor API failures. - // Missing stream collection/no fresh sample are treated as common non-fatal cases. for (auto& stream : m_streams) { - stream.reader->update(stream.tracked.data); + stream.reader->update(graph_time_ns, stream.tracked.data); } } diff --git a/src/core/live_trackers/cpp/live_frame_metadata_tracker_oak_impl.hpp b/src/core/live_trackers/cpp/live_frame_metadata_tracker_oak_impl.hpp index 790557785..d61488d25 100644 --- a/src/core/live_trackers/cpp/live_frame_metadata_tracker_oak_impl.hpp +++ b/src/core/live_trackers/cpp/live_frame_metadata_tracker_oak_impl.hpp @@ -40,7 +40,7 @@ class LiveFrameMetadataTrackerOakImpl : public IFrameMetadataTrackerOakImpl LiveFrameMetadataTrackerOakImpl(LiveFrameMetadataTrackerOakImpl&&) = delete; LiveFrameMetadataTrackerOakImpl& operator=(LiveFrameMetadataTrackerOakImpl&&) = delete; - void update(XrTime time) override; + void update(int64_t graph_time_ns) override; const FrameMetadataOakTrackedT& get_stream_data(size_t stream_index) const override; private: diff --git a/src/core/live_trackers/cpp/live_full_body_tracker_pico_impl.cpp b/src/core/live_trackers/cpp/live_full_body_tracker_pico_impl.cpp index e8457f356..fe99e3d99 100644 --- a/src/core/live_trackers/cpp/live_full_body_tracker_pico_impl.cpp +++ b/src/core/live_trackers/cpp/live_full_body_tracker_pico_impl.cpp @@ -97,10 +97,8 @@ LiveFullBodyTrackerPicoImpl::~LiveFullBodyTrackerPicoImpl() } } -void LiveFullBodyTrackerPicoImpl::update(XrTime time) +void LiveFullBodyTrackerPicoImpl::update(int64_t graph_time_ns) { - last_update_time_ = time; - if (body_tracker_ == XR_NULL_HANDLE) { // Policy: limp mode (feature unsupported/unavailable) is non-fatal. @@ -108,10 +106,13 @@ void LiveFullBodyTrackerPicoImpl::update(XrTime time) return; } + int64_t now_ns = os_monotonic_now_ns(); + XrTime xr_time = time_converter_.convert_monotonic_ns_to_xrtime(now_ns); + XrBodyJointsLocateInfoBD locate_info{ XR_TYPE_BODY_JOINTS_LOCATE_INFO_BD }; locate_info.next = nullptr; locate_info.baseSpace = base_space_; - locate_info.time = time; + locate_info.time = xr_time; XrBodyJointLocationBD joint_locations[XR_BODY_JOINT_COUNT_BD]; @@ -157,9 +158,10 @@ void LiveFullBodyTrackerPicoImpl::update(XrTime time) if (mcap_channels_) { - int64_t monotonic_ns = time_converter_.convert_xrtime_to_monotonic_ns(last_update_time_); - DeviceDataTimestamp timestamp(monotonic_ns, monotonic_ns, last_update_time_); - mcap_channels_->write(0, timestamp, tracked_.data); + // TODO: Replace with actual client-reported sample/available timestamps + // once the runtime exposes per-sample timing metadata. + DeviceDataTimestamp timestamp(now_ns, now_ns, static_cast(xr_time)); + mcap_channels_->write(0, graph_time_ns, timestamp, tracked_.data); } } diff --git a/src/core/live_trackers/cpp/live_full_body_tracker_pico_impl.hpp b/src/core/live_trackers/cpp/live_full_body_tracker_pico_impl.hpp index 11b033cf1..a453eb9c6 100644 --- a/src/core/live_trackers/cpp/live_full_body_tracker_pico_impl.hpp +++ b/src/core/live_trackers/cpp/live_full_body_tracker_pico_impl.hpp @@ -40,7 +40,7 @@ class LiveFullBodyTrackerPicoImpl : public IFullBodyTrackerPicoImpl LiveFullBodyTrackerPicoImpl(LiveFullBodyTrackerPicoImpl&&) = delete; LiveFullBodyTrackerPicoImpl& operator=(LiveFullBodyTrackerPicoImpl&&) = delete; - void update(XrTime time) override; + void update(int64_t graph_time_ns) override; const FullBodyPosePicoTrackedT& get_body_pose() const override; private: @@ -48,7 +48,6 @@ class LiveFullBodyTrackerPicoImpl : public IFullBodyTrackerPicoImpl XrSpace base_space_; XrBodyTrackerBD body_tracker_; FullBodyPosePicoTrackedT tracked_; - XrTime last_update_time_ = 0; PFN_xrCreateBodyTrackerBD pfn_create_body_tracker_; PFN_xrDestroyBodyTrackerBD pfn_destroy_body_tracker_; diff --git a/src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.cpp b/src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.cpp index ecf0a5179..b147a0434 100644 --- a/src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.cpp +++ b/src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.cpp @@ -44,11 +44,9 @@ LiveGeneric3AxisPedalTrackerImpl::LiveGeneric3AxisPedalTrackerImpl(const OpenXRS { } -void LiveGeneric3AxisPedalTrackerImpl::update(XrTime /*time*/) +void LiveGeneric3AxisPedalTrackerImpl::update(int64_t graph_time_ns) { - // Policy: SchemaTracker throws on critical OpenXR/tensor API failures. - // Missing collection/no new data are treated as common non-fatal cases. - m_schema_reader.update(m_tracked.data); + m_schema_reader.update(graph_time_ns, m_tracked.data); } const Generic3AxisPedalOutputTrackedT& LiveGeneric3AxisPedalTrackerImpl::get_data() const diff --git a/src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.hpp b/src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.hpp index a7841abd5..18a297232 100644 --- a/src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.hpp +++ b/src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.hpp @@ -38,7 +38,7 @@ class LiveGeneric3AxisPedalTrackerImpl : public IGeneric3AxisPedalTrackerImpl LiveGeneric3AxisPedalTrackerImpl(LiveGeneric3AxisPedalTrackerImpl&&) = delete; LiveGeneric3AxisPedalTrackerImpl& operator=(LiveGeneric3AxisPedalTrackerImpl&&) = delete; - void update(XrTime time) override; + void update(int64_t graph_time_ns) override; const Generic3AxisPedalOutputTrackedT& get_data() const override; private: diff --git a/src/core/live_trackers/cpp/live_hand_tracker_impl.cpp b/src/core/live_trackers/cpp/live_hand_tracker_impl.cpp index f35ed0829..7d15ec5b2 100644 --- a/src/core/live_trackers/cpp/live_hand_tracker_impl.cpp +++ b/src/core/live_trackers/cpp/live_hand_tracker_impl.cpp @@ -117,18 +117,20 @@ LiveHandTrackerImpl::~LiveHandTrackerImpl() } } -void LiveHandTrackerImpl::update(XrTime time) +void LiveHandTrackerImpl::update(int64_t graph_time_ns) { - last_update_time_ = time; - update_hand(left_hand_tracker_, time, left_tracked_); - update_hand(right_hand_tracker_, time, right_tracked_); + int64_t now_ns = os_monotonic_now_ns(); + XrTime xr_time = time_converter_.convert_monotonic_ns_to_xrtime(now_ns); + update_hand(left_hand_tracker_, xr_time, left_tracked_); + update_hand(right_hand_tracker_, xr_time, right_tracked_); if (mcap_channels_) { - int64_t monotonic_ns = time_converter_.convert_xrtime_to_monotonic_ns(last_update_time_); - DeviceDataTimestamp timestamp(monotonic_ns, monotonic_ns, last_update_time_); - mcap_channels_->write(0, timestamp, left_tracked_.data); - mcap_channels_->write(1, timestamp, right_tracked_.data); + // TODO: Replace with actual client-reported sample/available timestamps + // once the runtime exposes per-sample timing metadata. + DeviceDataTimestamp timestamp(now_ns, now_ns, static_cast(xr_time)); + mcap_channels_->write(0, graph_time_ns, timestamp, left_tracked_.data); + mcap_channels_->write(1, graph_time_ns, timestamp, right_tracked_.data); } } diff --git a/src/core/live_trackers/cpp/live_hand_tracker_impl.hpp b/src/core/live_trackers/cpp/live_hand_tracker_impl.hpp index 961d3f84f..dcde80572 100644 --- a/src/core/live_trackers/cpp/live_hand_tracker_impl.hpp +++ b/src/core/live_trackers/cpp/live_hand_tracker_impl.hpp @@ -38,12 +38,12 @@ class LiveHandTrackerImpl : public IHandTrackerImpl LiveHandTrackerImpl(LiveHandTrackerImpl&&) = delete; LiveHandTrackerImpl& operator=(LiveHandTrackerImpl&&) = delete; - void update(XrTime time) override; + void update(int64_t graph_time_ns) override; const HandPoseTrackedT& get_left_hand() const override; const HandPoseTrackedT& get_right_hand() const override; private: - void update_hand(XrHandTrackerEXT tracker, XrTime time, HandPoseTrackedT& tracked); + void update_hand(XrHandTrackerEXT tracker, XrTime xr_time, HandPoseTrackedT& tracked); XrTimeConverter time_converter_; XrSpace base_space_; @@ -53,7 +53,6 @@ class LiveHandTrackerImpl : public IHandTrackerImpl HandPoseTrackedT left_tracked_; HandPoseTrackedT right_tracked_; - XrTime last_update_time_ = 0; PFN_xrCreateHandTrackerEXT pfn_create_hand_tracker_; PFN_xrDestroyHandTrackerEXT pfn_destroy_hand_tracker_; diff --git a/src/core/live_trackers/cpp/live_head_tracker_impl.cpp b/src/core/live_trackers/cpp/live_head_tracker_impl.cpp index 6f6adbe78..e1c02fc8b 100644 --- a/src/core/live_trackers/cpp/live_head_tracker_impl.cpp +++ b/src/core/live_trackers/cpp/live_head_tracker_impl.cpp @@ -40,12 +40,13 @@ LiveHeadTrackerImpl::LiveHeadTrackerImpl(const OpenXRSessionHandles& handles, { } -void LiveHeadTrackerImpl::update(XrTime time) +void LiveHeadTrackerImpl::update(int64_t graph_time_ns) { - last_update_time_ = time; + int64_t now_ns = os_monotonic_now_ns(); + XrTime xr_time = time_converter_.convert_monotonic_ns_to_xrtime(now_ns); XrSpaceLocation location{ XR_TYPE_SPACE_LOCATION }; - XrResult result = core_funcs_.xrLocateSpace(view_space_.get(), base_space_, time, &location); + XrResult result = core_funcs_.xrLocateSpace(view_space_.get(), base_space_, xr_time, &location); if (XR_FAILED(result)) { @@ -78,9 +79,10 @@ void LiveHeadTrackerImpl::update(XrTime time) if (mcap_channels_) { - int64_t monotonic_ns = time_converter_.convert_xrtime_to_monotonic_ns(last_update_time_); - DeviceDataTimestamp timestamp(monotonic_ns, monotonic_ns, last_update_time_); - mcap_channels_->write(0, timestamp, tracked_.data); + // TODO: Replace with actual client-reported sample/available timestamps + // once the runtime exposes per-sample timing metadata. + DeviceDataTimestamp timestamp(now_ns, now_ns, static_cast(xr_time)); + mcap_channels_->write(0, graph_time_ns, timestamp, tracked_.data); } } diff --git a/src/core/live_trackers/cpp/live_head_tracker_impl.hpp b/src/core/live_trackers/cpp/live_head_tracker_impl.hpp index 5a9b525f5..83b53874e 100644 --- a/src/core/live_trackers/cpp/live_head_tracker_impl.hpp +++ b/src/core/live_trackers/cpp/live_head_tracker_impl.hpp @@ -36,7 +36,7 @@ class LiveHeadTrackerImpl : public IHeadTrackerImpl LiveHeadTrackerImpl(LiveHeadTrackerImpl&&) = delete; LiveHeadTrackerImpl& operator=(LiveHeadTrackerImpl&&) = delete; - void update(XrTime time) override; + void update(int64_t graph_time_ns) override; const HeadPoseTrackedT& get_head() const override; private: @@ -45,7 +45,6 @@ class LiveHeadTrackerImpl : public IHeadTrackerImpl XrSpace base_space_; XrSpacePtr view_space_; HeadPoseTrackedT tracked_; - XrTime last_update_time_ = 0; std::unique_ptr mcap_channels_; }; diff --git a/src/core/mcap/cpp/inc/mcap/tracker_channels.hpp b/src/core/mcap/cpp/inc/mcap/tracker_channels.hpp index 1cbe3c550..79920a846 100644 --- a/src/core/mcap/cpp/inc/mcap/tracker_channels.hpp +++ b/src/core/mcap/cpp/inc/mcap/tracker_channels.hpp @@ -57,7 +57,21 @@ class McapTrackerChannels } } - void write(size_t channel_index, const DeviceDataTimestamp& timestamp, const std::shared_ptr& data) + /** + * @brief Serialize a FlatBuffer record and write it to the MCAP channel. + * + * @param channel_index Sub-channel index (0-based) within this writer. + * @param log_time_ns Monotonic nanoseconds used as the MCAP envelope + * logTime and publishTime. Independent of the + * DeviceDataTimestamp fields embedded in the record. + * @param timestamp DeviceDataTimestamp embedded inside the FlatBuffer + * record payload (available, sample, raw-device times). + * @param data Data to serialize; null writes a timestamp-only record. + */ + void write(size_t channel_index, + int64_t log_time_ns, + const DeviceDataTimestamp& timestamp, + const std::shared_ptr& data) { if (channel_index >= channel_ids_.size()) { @@ -85,7 +99,7 @@ class McapTrackerChannels mcap::Message msg; msg.channelId = channel_ids_[channel_index]; - msg.logTime = static_cast(timestamp.available_time_local_common_clock()); + msg.logTime = static_cast(log_time_ns); msg.publishTime = msg.logTime; msg.sequence = sequence_++; msg.data = reinterpret_cast(builder.GetBufferPointer()); diff --git a/src/core/mcap_tests/cpp/test_mcap_tracker_channels.cpp b/src/core/mcap_tests/cpp/test_mcap_tracker_channels.cpp index 2af9101e1..cf51dd502 100644 --- a/src/core/mcap_tests/cpp/test_mcap_tracker_channels.cpp +++ b/src/core/mcap_tests/cpp/test_mcap_tracker_channels.cpp @@ -85,7 +85,7 @@ TEST_CASE("McapTrackerChannels: typed write produces readable MCAP with correct { auto writer = open_writer(path); HeadChannels ch(*writer, "tracking", core::HeadRecordingTraits::schema_name, { "head" }); - ch.write(0, core::DeviceDataTimestamp(1000000, 1000000, 42), head_data); + ch.write(0, 9999, core::DeviceDataTimestamp(1000000, 800000, 42), head_data); writer->close(); } @@ -97,11 +97,13 @@ TEST_CASE("McapTrackerChannels: typed write produces readable MCAP with correct { CHECK(view.channel->topic == "tracking/head"); CHECK(view.schema->name == core::HeadRecordingTraits::schema_name); - CHECK(view.message.logTime == 1000000); + CHECK(view.message.logTime == 9999); auto record = flatbuffers::GetRoot(view.message.data); REQUIRE(record != nullptr); REQUIRE(record->timestamp() != nullptr); + CHECK(record->timestamp()->available_time_local_common_clock() == 1000000); + CHECK(record->timestamp()->sample_time_local_common_clock() == 800000); CHECK(record->timestamp()->sample_time_raw_device_clock() == 42); REQUIRE(record->data() != nullptr); CHECK(record->data()->is_valid() == true); @@ -129,7 +131,7 @@ TEST_CASE("McapTrackerChannels: null data writes record with timestamp only", "[ { auto writer = open_writer(path); HeadChannels ch(*writer, "tracking", core::HeadRecordingTraits::schema_name, { "head" }); - ch.write(0, core::DeviceDataTimestamp(500, 500, 10), std::shared_ptr{ nullptr }); + ch.write(0, 500, core::DeviceDataTimestamp(500, 500, 10), std::shared_ptr{ nullptr }); writer->close(); } @@ -161,8 +163,8 @@ TEST_CASE("McapTrackerChannels: multi-channel write routes to correct topics", " { auto writer = open_writer(path); HeadChannels ch(*writer, "hands", core::HeadRecordingTraits::schema_name, { "left", "right" }); - ch.write(0, core::DeviceDataTimestamp(100, 100, 1), data); - ch.write(1, core::DeviceDataTimestamp(200, 200, 2), data); + ch.write(0, 100, core::DeviceDataTimestamp(100, 100, 1), data); + ch.write(1, 200, core::DeviceDataTimestamp(200, 200, 2), data); writer->close(); } @@ -190,7 +192,7 @@ TEST_CASE("McapTrackerChannels: out-of-range channel_index throws", "[mcap][trac auto writer = open_writer(path); HeadChannels ch(*writer, "test", core::HeadRecordingTraits::schema_name, { "only" }); - CHECK_THROWS_AS(ch.write(99, core::DeviceDataTimestamp(100, 100, 1), data), std::out_of_range); + CHECK_THROWS_AS(ch.write(99, 100, core::DeviceDataTimestamp(100, 100, 1), data), std::out_of_range); writer->close(); } @@ -204,9 +206,9 @@ TEST_CASE("McapTrackerChannels: sequence numbers increment across writes", "[mca { auto writer = open_writer(path); HeadChannels ch(*writer, "seq", core::HeadRecordingTraits::schema_name, { "ch" }); - ch.write(0, core::DeviceDataTimestamp(100, 100, 1), data); - ch.write(0, core::DeviceDataTimestamp(200, 200, 2), data); - ch.write(0, core::DeviceDataTimestamp(300, 300, 3), data); + ch.write(0, 100, core::DeviceDataTimestamp(100, 100, 1), data); + ch.write(0, 200, core::DeviceDataTimestamp(200, 200, 2), data); + ch.write(0, 300, core::DeviceDataTimestamp(300, 300, 3), data); writer->close(); } @@ -238,9 +240,9 @@ TEST_CASE("McapTrackerChannels: multiple same-type channel instances share one w HeadChannels head_ch(*writer, "head", core::HeadRecordingTraits::schema_name, { "pose" }); HeadChannels ctrl_ch(*writer, "ctrl", core::HeadRecordingTraits::schema_name, { "left", "right" }); - head_ch.write(0, core::DeviceDataTimestamp(100, 100, 1), data); - ctrl_ch.write(0, core::DeviceDataTimestamp(200, 200, 2), data); - ctrl_ch.write(1, core::DeviceDataTimestamp(300, 300, 3), data); + head_ch.write(0, 100, core::DeviceDataTimestamp(100, 100, 1), data); + ctrl_ch.write(0, 200, core::DeviceDataTimestamp(200, 200, 2), data); + ctrl_ch.write(1, 300, core::DeviceDataTimestamp(300, 300, 3), data); writer->close(); } diff --git a/src/core/oxr/cpp/inc/oxr/oxr_session.hpp b/src/core/oxr/cpp/inc/oxr/oxr_session.hpp index c95e0c4b9..1a23402a2 100644 --- a/src/core/oxr/cpp/inc/oxr/oxr_session.hpp +++ b/src/core/oxr/cpp/inc/oxr/oxr_session.hpp @@ -24,9 +24,9 @@ class OpenXRSession OpenXRSessionHandles get_handles() const; private: - using InstanceHandle = std::unique_ptr, decltype(&xrDestroyInstance)>; - using SessionHandle = std::unique_ptr, decltype(&xrDestroySession)>; - using SpaceHandle = std::unique_ptr, decltype(&xrDestroySpace)>; + using InstanceHandle = std::unique_ptr, PFN_xrDestroyInstance>; + using SessionHandle = std::unique_ptr, PFN_xrDestroySession>; + using SpaceHandle = std::unique_ptr, PFN_xrDestroySpace>; // Initialization methods void create_instance(const std::string& app_name, const std::vector& extensions); diff --git a/src/core/schema/fbs/hand.fbs b/src/core/schema/fbs/hand.fbs index 5274fce2f..a0ce49a26 100644 --- a/src/core/schema/fbs/hand.fbs +++ b/src/core/schema/fbs/hand.fbs @@ -6,6 +6,44 @@ include "timestamp.fbs"; namespace core; +// Hand joint indices for fixed-size hand pose arrays (e.g. HandJoints.poses). +// +// Ordinal values match Khronos OpenXR XrHandJointEXT (XR_EXT_hand_tracking): +// https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#XrHandJointEXT +// There are 26 joints (OpenXR XR_HAND_JOINT_COUNT_EXT). Enum member names follow +// the same ordering and naming as the OpenXR identifiers, omitting the +// XR_HAND_JOINT_ prefix and _EXT suffix (e.g. XR_HAND_JOINT_THUMB_TIP_EXT -> THUMB_TIP). +enum HandJoint : uint8 { + PALM = 0, + WRIST = 1, + THUMB_METACARPAL = 2, + THUMB_PROXIMAL = 3, + THUMB_DISTAL = 4, + THUMB_TIP = 5, + INDEX_METACARPAL = 6, + INDEX_PROXIMAL = 7, + INDEX_INTERMEDIATE = 8, + INDEX_DISTAL = 9, + INDEX_TIP = 10, + MIDDLE_METACARPAL = 11, + MIDDLE_PROXIMAL = 12, + MIDDLE_INTERMEDIATE = 13, + MIDDLE_DISTAL = 14, + MIDDLE_TIP = 15, + RING_METACARPAL = 16, + RING_PROXIMAL = 17, + RING_INTERMEDIATE = 18, + RING_DISTAL = 19, + RING_TIP = 20, + LITTLE_METACARPAL = 21, + LITTLE_PROXIMAL = 22, + LITTLE_INTERMEDIATE = 23, + LITTLE_DISTAL = 24, + LITTLE_TIP = 25, + + NUM_JOINTS = 26, +} + // Describe a HandJoint pose. struct HandJointPose { // The concrete pose data @@ -20,14 +58,15 @@ struct HandJointPose { } struct HandJoints { + // Fixed-size array; length must stay in sync with HandJoint::NUM_JOINTS (and the OpenXR joint count). poses: [HandJointPose:26]; } // Hand pose data. // All fields are always present when the parent Tracked/Record wrapper's data is non-null. table HandPose { - // Vector of HandJointPose. - // For OpenXR, this should be 26 according to XR_HAND_JOINT_COUNT_EXT + // Fixed-size hand joint poses in HandJoint / OpenXR order (struct HandJoints, not a variable-length vector). + // The HandJoints.poses array length must stay in sync with HandJoint::NUM_JOINTS. joints: HandJoints (id: 0); } diff --git a/src/core/schema/python/hand_bindings.h b/src/core/schema/python/hand_bindings.h index d9230e80b..a94280ab1 100644 --- a/src/core/schema/python/hand_bindings.h +++ b/src/core/schema/python/hand_bindings.h @@ -14,6 +14,7 @@ #include #include #include +#include #include namespace py = pybind11; @@ -23,6 +24,36 @@ namespace core inline void bind_hand(py::module& m) { + // Bind HandJoint enum (indices align with OpenXR XrHandJointEXT; see hand.fbs). + py::enum_(m, "HandJoint") + .value("PALM", HandJoint_PALM) + .value("WRIST", HandJoint_WRIST) + .value("THUMB_METACARPAL", HandJoint_THUMB_METACARPAL) + .value("THUMB_PROXIMAL", HandJoint_THUMB_PROXIMAL) + .value("THUMB_DISTAL", HandJoint_THUMB_DISTAL) + .value("THUMB_TIP", HandJoint_THUMB_TIP) + .value("INDEX_METACARPAL", HandJoint_INDEX_METACARPAL) + .value("INDEX_PROXIMAL", HandJoint_INDEX_PROXIMAL) + .value("INDEX_INTERMEDIATE", HandJoint_INDEX_INTERMEDIATE) + .value("INDEX_DISTAL", HandJoint_INDEX_DISTAL) + .value("INDEX_TIP", HandJoint_INDEX_TIP) + .value("MIDDLE_METACARPAL", HandJoint_MIDDLE_METACARPAL) + .value("MIDDLE_PROXIMAL", HandJoint_MIDDLE_PROXIMAL) + .value("MIDDLE_INTERMEDIATE", HandJoint_MIDDLE_INTERMEDIATE) + .value("MIDDLE_DISTAL", HandJoint_MIDDLE_DISTAL) + .value("MIDDLE_TIP", HandJoint_MIDDLE_TIP) + .value("RING_METACARPAL", HandJoint_RING_METACARPAL) + .value("RING_PROXIMAL", HandJoint_RING_PROXIMAL) + .value("RING_INTERMEDIATE", HandJoint_RING_INTERMEDIATE) + .value("RING_DISTAL", HandJoint_RING_DISTAL) + .value("RING_TIP", HandJoint_RING_TIP) + .value("LITTLE_METACARPAL", HandJoint_LITTLE_METACARPAL) + .value("LITTLE_PROXIMAL", HandJoint_LITTLE_PROXIMAL) + .value("LITTLE_INTERMEDIATE", HandJoint_LITTLE_INTERMEDIATE) + .value("LITTLE_DISTAL", HandJoint_LITTLE_DISTAL) + .value("LITTLE_TIP", HandJoint_LITTLE_TIP) + .value("NUM_JOINTS", HandJoint_NUM_JOINTS); + // Bind HandJointPose struct (pose, is_valid, radius). py::class_(m, "HandJointPose") .def(py::init<>()) @@ -44,22 +75,25 @@ inline void bind_hand(py::module& m) ", radius=" + std::to_string(self.radius()) + ")"; }); - // Bind HandJoints struct (fixed-size array of 26 HandJointPose). + // Bind HandJoints struct (fixed-size array; length matches HandJoint::NUM_JOINTS). py::class_(m, "HandJoints") .def(py::init<>()) .def( "poses", [](const HandJoints& self, size_t index) -> const HandJointPose* { - if (index >= 26) + if (index >= static_cast(HandJoint_NUM_JOINTS)) { - throw py::index_error("HandJoints index out of range (must be 0-25)"); + throw py::index_error("HandJoints index out of range (must be 0-" + + std::to_string(static_cast(HandJoint_NUM_JOINTS) - 1) + ")"); } return (*self.poses())[index]; }, py::arg("index"), py::return_value_policy::reference_internal, - "Get the HandJointPose at the specified index (0-25).") - .def("__repr__", [](const HandJoints&) { return "HandJoints(poses=[...26 HandJointPose entries...])"; }); + "Get the HandJointPose at the specified index. Valid indices: 0 <= index < HandJoint.NUM_JOINTS " + "(OpenXR hand joint order).") + .def("__repr__", + [](const HandJoints&) { return "HandJoints(poses=[...HandJoint.NUM_JOINTS HandJointPose entries...])"; }); // Bind HandPoseT class (FlatBuffers object API for tables). py::class_>(m, "HandPoseT") diff --git a/src/core/schema/python/schema_init.py b/src/core/schema/python/schema_init.py index 22f57f3a2..e5ab8c893 100644 --- a/src/core/schema/python/schema_init.py +++ b/src/core/schema/python/schema_init.py @@ -19,6 +19,7 @@ HeadPoseTrackedT, HeadPoseRecord, # Hand-related types. + HandJoint, HandJointPose, HandJoints, HandPoseT, @@ -61,6 +62,7 @@ "HeadPoseTrackedT", "HeadPoseRecord", # Hand types. + "HandJoint", "HandJointPose", "HandJoints", "HandPoseT", diff --git a/src/core/schema_tests/cpp/CMakeLists.txt b/src/core/schema_tests/cpp/CMakeLists.txt index 0e28140af..f8815306d 100644 --- a/src/core/schema_tests/cpp/CMakeLists.txt +++ b/src/core/schema_tests/cpp/CMakeLists.txt @@ -15,6 +15,7 @@ add_executable(schema_tests target_link_libraries(schema_tests PRIVATE isaacteleop_schema Catch2::Catch2WithMain + OpenXR::headers ) message(STATUS "schema_tests target enabled with Catch2") diff --git a/src/core/schema_tests/cpp/test_hand.cpp b/src/core/schema_tests/cpp/test_hand.cpp index c4171099e..8eace29b2 100644 --- a/src/core/schema_tests/cpp/test_hand.cpp +++ b/src/core/schema_tests/cpp/test_hand.cpp @@ -6,6 +6,7 @@ #include #include #include +#include // Include generated FlatBuffer headers. #include @@ -39,9 +40,47 @@ static_assert(std::is_trivially_copyable_v, "HandJointPose // ============================================================================= static_assert(std::is_trivially_copyable_v, "HandJoints should be a trivially copyable struct"); -// HandJoints should contain exactly 26 HandJointPose entries. -static_assert(sizeof(core::HandJoints) == 26 * sizeof(core::HandJointPose), - "HandJoints should contain exactly 26 HandJointPose entries"); +// HandJoints should contain exactly HandJoint::NUM_JOINTS HandJointPose entries. +static_assert(sizeof(core::HandJoints) == core::HandJoint_NUM_JOINTS * sizeof(core::HandJointPose), + "HandJoints size must match HandJoint::NUM_JOINTS"); + +// ============================================================================= +// OpenXR parity: core::HandJoint ordinals must match XrHandJointEXT (XR_EXT_hand_tracking). +// ============================================================================= +static_assert(static_cast(core::HandJoint_NUM_JOINTS) == XR_HAND_JOINT_COUNT_EXT, + "HandJoint::NUM_JOINTS must match OpenXR XR_HAND_JOINT_COUNT_EXT"); +static_assert(static_cast(core::HandJoint_PALM) == static_cast(XR_HAND_JOINT_PALM_EXT)); +static_assert(static_cast(core::HandJoint_WRIST) == static_cast(XR_HAND_JOINT_WRIST_EXT)); +static_assert(static_cast(core::HandJoint_THUMB_METACARPAL) == static_cast(XR_HAND_JOINT_THUMB_METACARPAL_EXT)); +static_assert(static_cast(core::HandJoint_THUMB_PROXIMAL) == static_cast(XR_HAND_JOINT_THUMB_PROXIMAL_EXT)); +static_assert(static_cast(core::HandJoint_THUMB_DISTAL) == static_cast(XR_HAND_JOINT_THUMB_DISTAL_EXT)); +static_assert(static_cast(core::HandJoint_THUMB_TIP) == static_cast(XR_HAND_JOINT_THUMB_TIP_EXT)); +static_assert(static_cast(core::HandJoint_INDEX_METACARPAL) == static_cast(XR_HAND_JOINT_INDEX_METACARPAL_EXT)); +static_assert(static_cast(core::HandJoint_INDEX_PROXIMAL) == static_cast(XR_HAND_JOINT_INDEX_PROXIMAL_EXT)); +static_assert(static_cast(core::HandJoint_INDEX_INTERMEDIATE) == + static_cast(XR_HAND_JOINT_INDEX_INTERMEDIATE_EXT)); +static_assert(static_cast(core::HandJoint_INDEX_DISTAL) == static_cast(XR_HAND_JOINT_INDEX_DISTAL_EXT)); +static_assert(static_cast(core::HandJoint_INDEX_TIP) == static_cast(XR_HAND_JOINT_INDEX_TIP_EXT)); +static_assert(static_cast(core::HandJoint_MIDDLE_METACARPAL) == + static_cast(XR_HAND_JOINT_MIDDLE_METACARPAL_EXT)); +static_assert(static_cast(core::HandJoint_MIDDLE_PROXIMAL) == static_cast(XR_HAND_JOINT_MIDDLE_PROXIMAL_EXT)); +static_assert(static_cast(core::HandJoint_MIDDLE_INTERMEDIATE) == + static_cast(XR_HAND_JOINT_MIDDLE_INTERMEDIATE_EXT)); +static_assert(static_cast(core::HandJoint_MIDDLE_DISTAL) == static_cast(XR_HAND_JOINT_MIDDLE_DISTAL_EXT)); +static_assert(static_cast(core::HandJoint_MIDDLE_TIP) == static_cast(XR_HAND_JOINT_MIDDLE_TIP_EXT)); +static_assert(static_cast(core::HandJoint_RING_METACARPAL) == static_cast(XR_HAND_JOINT_RING_METACARPAL_EXT)); +static_assert(static_cast(core::HandJoint_RING_PROXIMAL) == static_cast(XR_HAND_JOINT_RING_PROXIMAL_EXT)); +static_assert(static_cast(core::HandJoint_RING_INTERMEDIATE) == + static_cast(XR_HAND_JOINT_RING_INTERMEDIATE_EXT)); +static_assert(static_cast(core::HandJoint_RING_DISTAL) == static_cast(XR_HAND_JOINT_RING_DISTAL_EXT)); +static_assert(static_cast(core::HandJoint_RING_TIP) == static_cast(XR_HAND_JOINT_RING_TIP_EXT)); +static_assert(static_cast(core::HandJoint_LITTLE_METACARPAL) == + static_cast(XR_HAND_JOINT_LITTLE_METACARPAL_EXT)); +static_assert(static_cast(core::HandJoint_LITTLE_PROXIMAL) == static_cast(XR_HAND_JOINT_LITTLE_PROXIMAL_EXT)); +static_assert(static_cast(core::HandJoint_LITTLE_INTERMEDIATE) == + static_cast(XR_HAND_JOINT_LITTLE_INTERMEDIATE_EXT)); +static_assert(static_cast(core::HandJoint_LITTLE_DISTAL) == static_cast(XR_HAND_JOINT_LITTLE_DISTAL_EXT)); +static_assert(static_cast(core::HandJoint_LITTLE_TIP) == static_cast(XR_HAND_JOINT_LITTLE_TIP_EXT)); // ============================================================================= // HandJointPose Tests @@ -88,9 +127,9 @@ TEST_CASE("HandJointPose default construction", "[hand][struct]") // ============================================================================= TEST_CASE("HandJoints struct has correct size", "[hand][struct]") { - // HandJoints should have exactly 26 entries (XR_HAND_JOINT_COUNT_EXT). + // HandJoints should have exactly HandJoint::NUM_JOINTS entries. core::HandJoints joints; - CHECK(joints.poses()->size() == 26); + CHECK(joints.poses()->size() == static_cast(core::HandJoint_NUM_JOINTS)); } TEST_CASE("HandJoints can be accessed by index", "[hand][struct]") @@ -99,7 +138,7 @@ TEST_CASE("HandJoints can be accessed by index", "[hand][struct]") // Access first and last entries (returns pointers). const auto* first = (*joints.poses())[0]; - const auto* last = (*joints.poses())[25]; + const auto* last = (*joints.poses())[static_cast(core::HandJoint_NUM_JOINTS) - 1]; // Default values should be zero. CHECK(first->pose().position().x() == 0.0f); @@ -124,7 +163,7 @@ TEST_CASE("HandPoseT can store joints data", "[hand][native]") // Create and set joints. hand_pose->joints = std::make_unique(); - CHECK(hand_pose->joints->poses()->size() == 26); + CHECK(hand_pose->joints->poses()->size() == static_cast(core::HandJoint_NUM_JOINTS)); } TEST_CASE("HandPoseT joints can be mutated via flatbuffers Array", "[hand][native]") @@ -175,7 +214,7 @@ TEST_CASE("HandPoseT serialization and deserialization", "[hand][flatbuffers]") auto deserialized = flatbuffers::GetRoot(buffer); // Verify. - CHECK(deserialized->joints()->poses()->size() == 26); + CHECK(deserialized->joints()->poses()->size() == static_cast(core::HandJoint_NUM_JOINTS)); const auto* first_joint = (*deserialized->joints()->poses())[0]; CHECK(first_joint->pose().position().x() == Catch::Approx(1.5f)); @@ -194,7 +233,7 @@ TEST_CASE("HandPoseT can be unpacked from buffer", "[hand][flatbuffers]") original->joints = std::make_unique(); // Set multiple joint poses - for (size_t i = 0; i < 26; ++i) + for (size_t i = 0; i < static_cast(core::HandJoint_NUM_JOINTS); ++i) { core::Point position(static_cast(i), static_cast(i * 2), static_cast(i * 3)); core::Quaternion orientation(0.0f, 0.0f, 0.0f, 1.0f); @@ -218,19 +257,20 @@ TEST_CASE("HandPoseT can be unpacked from buffer", "[hand][flatbuffers]") CHECK(joint_5->pose().position().y() == Catch::Approx(10.0f)); CHECK(joint_5->pose().position().z() == Catch::Approx(15.0f)); - const auto* joint_25 = (*unpacked->joints->poses())[25]; - CHECK(joint_25->pose().position().x() == Catch::Approx(25.0f)); - CHECK(joint_25->pose().position().y() == Catch::Approx(50.0f)); - CHECK(joint_25->pose().position().z() == Catch::Approx(75.0f)); + const size_t last_joint_index = static_cast(core::HandJoint_NUM_JOINTS) - 1; + const auto* joint_last = (*unpacked->joints->poses())[last_joint_index]; + CHECK(joint_last->pose().position().x() == Catch::Approx(static_cast(last_joint_index))); + CHECK(joint_last->pose().position().y() == Catch::Approx(static_cast(last_joint_index * 2))); + CHECK(joint_last->pose().position().z() == Catch::Approx(static_cast(last_joint_index * 3))); } -TEST_CASE("HandPoseT all 26 joints can be set and verified", "[hand][native]") +TEST_CASE("HandPoseT all joints can be set and verified", "[hand][native]") { auto hand_pose = std::make_unique(); hand_pose->joints = std::make_unique(); - // Set all 26 joints with unique positions. - for (size_t i = 0; i < 26; ++i) + // Set every joint slot with a unique position. + for (size_t i = 0; i < static_cast(core::HandJoint_NUM_JOINTS); ++i) { core::Point position(static_cast(i), 0.0f, 0.0f); core::Quaternion orientation(0.0f, 0.0f, 0.0f, 1.0f); @@ -240,7 +280,7 @@ TEST_CASE("HandPoseT all 26 joints can be set and verified", "[hand][native]") } // Verify all joints. - for (size_t i = 0; i < 26; ++i) + for (size_t i = 0; i < static_cast(core::HandJoint_NUM_JOINTS); ++i) { const auto* joint = (*hand_pose->joints->poses())[i]; CHECK(joint->pose().position().x() == Catch::Approx(static_cast(i))); @@ -268,7 +308,7 @@ TEST_CASE("HandPoseRecord serialization with DeviceDataTimestamp", "[hand][flatb CHECK(deserialized->timestamp()->available_time_local_common_clock() == 1000000000LL); CHECK(deserialized->timestamp()->sample_time_local_common_clock() == 2000000000LL); CHECK(deserialized->timestamp()->sample_time_raw_device_clock() == 3000000000LL); - CHECK(deserialized->data()->joints()->poses()->size() == 26); + CHECK(deserialized->data()->joints()->poses()->size() == static_cast(core::HandJoint_NUM_JOINTS)); } TEST_CASE("HandPoseRecord can be unpacked with DeviceDataTimestamp", "[hand][flatbuffers]") @@ -290,5 +330,5 @@ TEST_CASE("HandPoseRecord can be unpacked with DeviceDataTimestamp", "[hand][fla CHECK(unpacked->timestamp->available_time_local_common_clock() == 111LL); CHECK(unpacked->timestamp->sample_time_local_common_clock() == 222LL); CHECK(unpacked->timestamp->sample_time_raw_device_clock() == 333LL); - CHECK(unpacked->data->joints->poses()->size() == 26); + CHECK(unpacked->data->joints->poses()->size() == static_cast(core::HandJoint_NUM_JOINTS)); } diff --git a/src/core/schema_tests/python/test_hand.py b/src/core/schema_tests/python/test_hand.py index 5afbdcd37..dbdaef304 100644 --- a/src/core/schema_tests/python/test_hand.py +++ b/src/core/schema_tests/python/test_hand.py @@ -4,9 +4,9 @@ """Unit tests for HandPoseT and related types in isaacteleop.schema. HandPoseT is a FlatBuffers table that represents hand pose data: -- joints: HandJoints struct containing 26 HandJointPose entries (XR_HAND_JOINT_COUNT_EXT) +- joints: HandJoints struct with a fixed-size poses array (length HandJoint.NUM_JOINTS; OpenXR order) -HandJoints is a struct with a fixed-size array of 26 HandJointPose entries. +HandJoints is a struct with a fixed-size array of HandJointPose (length HandJoint.NUM_JOINTS). HandJointPose is a struct containing: - pose: The Pose (position and orientation) @@ -19,17 +19,27 @@ import pytest from isaacteleop.schema import ( - HandPoseT, - HandPoseRecord, - HandJoints, + DeviceDataTimestamp, + HandJoint, HandJointPose, - Pose, + HandJoints, + HandPoseRecord, + HandPoseT, Point, + Pose, Quaternion, - DeviceDataTimestamp, ) +def test_hand_joint_enum_sentinels(): + """HandJoint ordinals match expected OpenXR-style layout.""" + assert HandJoint.PALM == 0 + assert HandJoint.WRIST == 1 + assert HandJoint.THUMB_TIP == 5 + assert HandJoint.LITTLE_TIP == 25 + assert HandJoint.NUM_JOINTS == 26 + + class TestHandJointPoseConstruction: """Tests for HandJointPose construction.""" @@ -105,10 +115,10 @@ class TestHandJointsStruct: """Tests for HandJoints struct.""" def test_poses_access(self): - """Test accessing all 26 joints via poses() method.""" + """Test accessing every joint slot via poses() method.""" hand_joints = HandJoints() - for i in range(26): + for i in range(HandJoint.NUM_JOINTS): joint = hand_joints.poses(i) assert joint is not None @@ -117,7 +127,7 @@ def test_poses_out_of_range(self): hand_joints = HandJoints() with pytest.raises(IndexError): - _ = hand_joints.poses(26) + _ = hand_joints.poses(HandJoint.NUM_JOINTS) class TestHandJointsRepr: diff --git a/src/core/teleop_session_manager/python/teleop_session.py b/src/core/teleop_session_manager/python/teleop_session.py index 128294117..ed7b3d81b 100644 --- a/src/core/teleop_session_manager/python/teleop_session.py +++ b/src/core/teleop_session_manager/python/teleop_session.py @@ -278,8 +278,16 @@ def step( if self.frame_count % 60 == 0: self._check_plugin_health() - # Update DeviceIO session (polls trackers) - self.deviceio_session.update() + # Resolve graph time before updating DeviceIO so the same logical + # timestamp is used for both MCAP envelope and pipeline execution. + if graph_time is None: + now_ns = time.monotonic_ns() + graph_time = GraphTime(sim_time_ns=now_ns, real_time_ns=now_ns) + + # Update DeviceIO session (polls trackers). The graph time is used for + # MCAP logTime; each tracker impl measures its own wall-clock "now" for + # DeviceDataTimestamp. + self.deviceio_session.update(graph_time.real_time_ns) # Build input dictionary from tracker data pipeline_inputs = self._collect_tracker_data() @@ -288,10 +296,6 @@ def step( if external_inputs: pipeline_inputs.update(external_inputs) - now_ns = time.monotonic_ns() - if graph_time is None: - graph_time = GraphTime(sim_time_ns=now_ns, real_time_ns=now_ns) - if execution_events is None and self.teleop_control_pipeline is not None: # Filter pipeline_inputs to only control leaf inputs control_inputs = { diff --git a/src/plugins/controller_synthetic_hands/synthetic_hands_plugin.cpp b/src/plugins/controller_synthetic_hands/synthetic_hands_plugin.cpp index 4501c23db..577368217 100644 --- a/src/plugins/controller_synthetic_hands/synthetic_hands_plugin.cpp +++ b/src/plugins/controller_synthetic_hands/synthetic_hands_plugin.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include #include @@ -73,7 +74,7 @@ void SyntheticHandsPlugin::worker_thread() try { // Update DeviceIOSession (handles time and tracker updates) - m_deviceio_session->update(); + m_deviceio_session->update(core::os_monotonic_now_ns()); // Read tracker data in the same exception boundary as update. left_tracked = m_controller_tracker->get_left_controller(*m_deviceio_session); diff --git a/src/plugins/manus/core/manus_hand_tracking_plugin.cpp b/src/plugins/manus/core/manus_hand_tracking_plugin.cpp index 0d3d75800..018068e45 100644 --- a/src/plugins/manus/core/manus_hand_tracking_plugin.cpp +++ b/src/plugins/manus/core/manus_hand_tracking_plugin.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -36,7 +37,7 @@ ManusTracker& ManusTracker::instance(const std::string& app_name) noexcept(false void ManusTracker::update() { // Update DeviceIOSession which handles time conversion and tracker updates internally - m_deviceio_session->update(); + m_deviceio_session->update(core::os_monotonic_now_ns()); inject_hand_data(); }