HandTracker: sample timestamps of hands injected via push devices are lost; MCAP `sample_time` is the query time
Maintainer thường phản hồi trong vòng 1 ngày
@nv-jakob đang làm issue này rồi.
Từ ngày 28/10/2026.
Đánh giá
Issue này chưa được đánh giá.
Mô tả
Summary
We are developing an IsaacCapture plugin for a data glove (RevoHuman) and plan to open a PR later. Before that, we would like to confirm the timestamp convention for hand data.
When the plugin pushes hand data, it fills XrPushDeviceHandTrackingDataNV.timestamp with the glove's sample time. In our tests, CloudXR Runtime 6.3.0 does not use this timestamp when selecting a frame, and LiveHandTrackerImpl then records the query time as sample_time. As a result, the hand pose sample_time in the MCAP is later than the actual sample time, and the original sample time cannot be recovered from the recording. Tactile data from the same glove frame goes through the tensor path, where sample_time is recorded correctly, so the two end up about 10–15 ms apart in the same MCAP.
Background: the RevoHuman plugin
The plugin is structured like the Manus plugin, with one pipeline per hand:
- Input: the vendor SDK delivers joint angles and tactile data for each hand at about 200 Hz, with the glove sample time (CLOCK_MONOTONIC) on every frame.
- Hand joints: FK produces the 26 OpenXR hand joints; the wrist pose is provided externally. Joints are injected with
HandInjector::push(joints, t), wheretis the glove sample time converted to XrTime. In steady state,tis about 9.5 ms before the push. - Tactile: pushed as a tensor via
SchemaPusher, withsample_timealso set to the glove sample time. - Stall handling: if the glove stream stalls for more than 50 ms, the plugin destroys the injector and the reader goes inactive.
Current behavior
- The
timestampfield of the push struct has no documented semantics (XR_NVX1_device_interface.h#L106-L116). Existing hand plugins all pass the push time, e.g. Manus (#L1285) and Sharpa (#L702). - Runtime 6.3.0 returns only the most recently arrived frame and ignores the pushed timestamp (measurements below).
LiveHandTrackerImplrecordsDeviceDataTimestamp(last_update_time_, last_update_time_, xr_time), i.e. available = sample = the query time of the current update (live_hand_tracker_impl.cpp#L188). This matches the convention in live_trackers/AGENTS.md#L16-L19 and appears to be intentional, following the OpenXR query/prediction model.- However, timestamp.fbs defines
sample_timeas the time "when the sample was taken", which "enables synchronization across different data sources". The tensor path (schema_tracker_base.cpp#L287-L295) satisfies this definition; the push-device hand path does not.
Impact: for hands coming from a push device, MCAP sample_time − actual sample time = (sample-to-push latency) + (phase offset between push and query). For RevoHuman at 200 Hz, this is about 9.5 ms + 0–5 ms. The second term grows as the push rate drops (p99 of 98 ms at 10 Hz).
Reproduction
No glove is needed; a minimal OpenXR program is enough (C++, depends only on the OpenXR loader; we can share it):
- Start CloudXR Runtime 6.3.0 with
NV_CXR_ENABLE_PUSH_DEVICES=true. - Process A:
xrCreatePushDeviceNV(right hand), then callxrPushDevicePushHandTrackingNVin a loop withtimestamp = now + Δ, encoding the frame index in the joint positions. - Process B: call
xrLocateHandJointsEXTthe same way IC does, with varioustimevalues, and decode which frame was returned from the joint positions.
Results (3 same-process runs and 7 cross-process runs, all consistent):
| Action | Result |
|---|---|
| Push timestamp Δ ∈ {−100, −20, 0, +20, +100} ms | Same frame returned; the pushed timestamp is only echoed back |
| Query time swept from −1 s to +5 s | Always the most recently arrived frame: no interpolation, no extrapolation (velocityFlags always 0), no history |
| Out-of-order push (the later frame carries an older timestamp) | The later-arriving frame overwrites the earlier one |
| Process alive but stops pushing for 30 s | Stays isActive=true, frozen at the last frame |
Push jointCount=0 / destroy the push device / process exits |
Inactive within one read cycle (<10 ms) |
Expected behavior (for discussion)
Any one of the following would work for us:
- A: For push-device hands, HandTracker records the pushed timestamp as
sample_time. This requires the runtime to expose that timestamp to the reader. - B: If the hand path intentionally keeps the query time, recommend an official side channel, e.g. the plugin also pushes a tensor carrying
sample_time(the existingsoma_hand_joint_poses? trackers.toml#L56-L61), and describe how it maps to HandPose. - C: At minimum, document three points:
timestampis not used for frame selection; HandPosesample_timeis the query time; producers are responsible for stall detection.
Questions
- Should
XrPushDeviceHandTrackingDataNV.timestampcarry the sample time or the push time? - Is the 6.3.0 behavior above (latest arrival wins, timestamp ignored, no expiry) by design, or a property of the current implementation? Will it change in later versions?
- Are there plans for HandPose to record the actual sample time? If not, which option do you recommend?
- While a process is alive but has stopped pushing, the hand stays frozen and is reported as active. Is there a recommended threshold for how long a producer should wait after a stall before destroying the injector? We use 50 ms; existing plugins range from 200 ms to 3 s, e.g. Wuji destroys the injector after 200 ms (#L554-L560), while Sharpa treats the glove as disconnected only after 3 s without data (#L497-L500). (Related: #1319)
Environment
- IsaacCapture: code references at main
a74babb0; tests run on9aee7654(hand-related files are identical in both) - CloudXR Runtime: 6.3.0
- OS: Ubuntu 22.04 (x86_64)
- No headset:
XR_MND_headless+XR_EXTX_overlay, STAGE space
- Ngôn ngữ chính
- Python
- Star
- 425
- Fork
- 101
- Merge trung bình
- 1 ngày 14 giờ
- Pull request đã merge (30 ngày)
- 165
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của NVIDIA/IsaacCapture
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
NVIDIA/IsaacCapture#910 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 88/100
NVIDIA/IsaacCapture#891 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
cloudxr: possible exec race in background.start_and_wait() (needs owner triage)Có thể đã có người làm @nv-jakob đã nhận 3 ngày trước. Đang mởNVIDIA
NVIDIA/IsaacCapture#1378 · 1 bình luận · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Expose per-glove health and data freshness in the Avatar SDK and Isaac Teleop pluginCó thể đã có người làm @zihanjiang321 đã nhận 4 ngày trước. Đang mở
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 35/100
NVIDIA/IsaacCapture#1319 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Add OneRobotics A1 asset and Isaac Teleop exampleCó thể đã có người làm @WangXuechun-woan đã nhận 11 ngày trước. Đang mở
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 35/100
NVIDIA/IsaacCapture#1291 · 2 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của NVIDIA/IsaacCapture
Issue tương tự
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 86/100
NousResearch/hermes-agent#147104 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
premises-missing
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 88/100
WingedGuardian/GENesis-AGI#3801 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
[oblt-aw][autodoc] Document new required `shared-org-key` input for `obs-aw-dependency-review`Có thể đã có người làm @github-actions đã nhận hôm nay. Đang mở
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 90/100
Maintainer thường phản hồi trong vòng 1 ngày
-
bot-triaged module: inductor oncall: pt2 topic: fuzzer triaged
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 4 ngày