feat(webxr): XR input recording, replay, and hand/controller path trace#765
feat(webxr): XR input recording, replay, and hand/controller path trace#765yanziz-nvidia wants to merge 8 commits into
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Enterprise Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThe CloudXR WebXR client adds XR input recording and replay, JSON import/export, live and recorded hand/controller trace visualization, and configurable in-XR controls. App startup, capability checks, session handling, teleoperation messaging, OOB control, metrics reporting, and disconnect behavior are updated. Configuration and HTML settings now persist trace and recording-control options through runtime configuration. Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant CloudXR3DUI
participant RecorderContext
participant XRInputRecorder
participant RecorderComponent
User->>CloudXR3DUI: Start recording
CloudXR3DUI->>RecorderContext: startRecord()
RecorderContext->>XRInputRecorder: startRecording()
RecorderComponent->>XRInputRecorder: beginFrame(XRFrame)
XRInputRecorder-->>RecorderContext: recorded frame count
User->>CloudXR3DUI: Stop, replay, or save recording
CloudXR3DUI->>RecorderContext: invoke recording action
RecorderContext->>XRInputRecorder: stop, replay, or export recording
sequenceDiagram
participant AppContent
participant CloudXRSession
participant MessageChannel
participant HeadsetControlChannel
AppContent->>CloudXRSession: enter immersive session
AppContent->>MessageChannel: send start or reset command
MessageChannel-->>AppContent: receive teleoperation data
CloudXRSession-->>AppContent: report stream status and performance
AppContent->>HeadsetControlChannel: send status and metrics
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
bac7d71 to
5cbf0e5
Compare
… trace Adds two opt-in troubleshooting tools for the IsaacTeleop web client. Both are hidden by default and enabled from the 2D settings panel. New files: - xrInputRecorder.ts: frame-indexed recorder that monkey-patches XRFrame.getPose, getJointPose, and XRSession.inputSources to capture and replay grip/aim poses, hand joint arrays, and gamepad button/axis state; supports JSON save/load - RecorderContext.tsx / RecorderComponent.tsx: React context + null- rendering R3F component that drives recorder.beginFrame() at priority -1001 (before CloudXR SDK at -1000); live frame count via onFrameRecord - TraceVisualization.tsx: rolling 500-point path trace (6px screen-space dots, sizeAttenuation:false for WebXR stereo) for grip and wrist; queries live XR poses in idle mode, recorder.currentFrame in replay Modified files: - index.html / CloudXR2DUI.tsx: "Hand/Controller Trace" select (In-XR Comfort) and "Recording Controls" select (Troubleshooting); both default hidden, persisted to localStorage; URL-param overrideable - CloudXRUI.tsx: conditional Recording section (Record/Stop, Play/Stop, Save, Load) in the in-XR overlay, shown when showRecordingControls=true - App.tsx: wraps AppContent in RecorderProvider; mounts RecorderComponent and TraceVisualization inside the R3F canvas - params.ts / resolve.test.ts: register showTraceInXR and showRecordingControls URL params Signed-off-by: Yanzi Zhu <yanziz@nvidia.com> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: yanziz-nv <yanziz@nvidia.com>
6b730b1 to
7e5366f
Compare
Covers mode state machine, frame accumulation, currentFrame per mode, replay loop/no-loop/connected=false gate, JSON round-trip, getRecording snapshot isolation, and XRFrame.prototype patch install/restore. WebXR globals are stubbed (FakeXRFrame, FakeXRSession) via beforeAll so the tests run in Jest's node environment without a browser. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: yanziz-nv <yanziz@nvidia.com>
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (1)
deps/cloudxr/webxr_client/src/TraceVisualization.tsx (1)
180-254: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winPer-frame allocations in the idle live-pose fallback path.
Every XR frame when not recording/replaying, this block allocates
livePoses,liveJoints, and a new pose/joint object for every tracked source and joint (up to 25 joints × 2 hands). The file's own doc comment states geometry is pre-allocated specifically to "run at full XR frame rate," but this fallback path directly contradicts that by generating GC pressure at 72-120Hz — a known cause of frame judder in VR.Consider reusing a small set of persistent scratch objects (mutate in place) instead of allocating fresh objects/arrays each frame.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@deps/cloudxr/webxr_client/src/TraceVisualization.tsx` around lines 180 - 254, Eliminate per-frame allocations in the live fallback within useFrame by adding persistent, reusable scratch pose objects, joint arrays, and frame containers outside the callback. Mutate and reset these structures in place for each XR frame, including clearing absent poses/joints and tracking hasJoints, then assign the reused container to frame. Avoid creating objects or arrays inside the input-source and joint loops.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@deps/cloudxr/webxr_client/src/App.tsx`:
- Around line 159-171: Wire the JSON load flow end-to-end: destructure
onLoadRecording from useRecorder() in AppContent, pass it into CloudXR3DUI, add
the corresponding onLoadRecording prop to CloudXRUIProps, and render a Load
button in the recording panel that invokes it alongside the existing Save
control.
In `@deps/cloudxr/webxr_client/src/index.html`:
- Around line 1066-1069: Escape the raw greater-than character in the config
guidance text by replacing “>=” with “>=” within the config-text div.
In `@deps/cloudxr/webxr_client/src/RecorderComponent.tsx`:
- Around line 53-57: Update the visibility logic in the recorder frame handling:
since the comparison assigned to isVisible always produces a boolean, remove the
dead ?? true fallback and explicitly default missing sessions to the intended
visibility behavior. Adjust the session visibility expression and the
recorder.beginFrame call so a null session is handled consistently with the
intended default.
In `@deps/cloudxr/webxr_client/src/TraceVisualization.tsx`:
- Around line 172-176: Assign distinct colors to the leftWrist and rightWrist
channels in the trace channel configuration, matching the per-hand color
distinction already used by leftGrip and rightGrip. Update one of the duplicate
"`#ff4422`" values while preserving the makeTraceChannel setup.
- Around line 256-274: Update updateTrace so slot.points.visible is set to false
whenever showTrace is false or frame/pose is unavailable; only append data,
update geometry, and show the points when all required inputs exist, ensuring
stale trace dots are hidden during tracking loss.
In `@deps/cloudxr/webxr_client/src/xrInputRecorder.ts`:
- Around line 333-338: Validate the parsed recording structure in static
importJSON, not just version: ensure frames exists and is an array before
returning the Recording, and throw a descriptive error for malformed input. Keep
this validation inside importJSON so RecorderContext.onLoadRecording can handle
invalid files before the render loop starts.
- Around line 455-466: The replay branch of _onGetPose only handles gripSpace,
so recorded target-ray poses are not replayed. Extend the input-source matching
to recognize src.targetRaySpace and return makeFakePose using
_currentReplay.poses.leftAim or rightAim according to src.handedness, while
preserving the existing grip-space behavior and fallback to _origGetPose.
- Around line 248-315: Gate the recording branch in beginFrame with connected,
matching the existing replay behavior and RecorderComponent.tsx intent. Update
the condition around the recording logic so frames are captured only when
connected; preserve replay handling and ensure disconnected recording does not
append entries or update _currentEntry.
---
Nitpick comments:
In `@deps/cloudxr/webxr_client/src/TraceVisualization.tsx`:
- Around line 180-254: Eliminate per-frame allocations in the live fallback
within useFrame by adding persistent, reusable scratch pose objects, joint
arrays, and frame containers outside the callback. Mutate and reset these
structures in place for each XR frame, including clearing absent poses/joints
and tracking hasJoints, then assign the reused container to frame. Avoid
creating objects or arrays inside the input-source and joint loops.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 9e79ad35-4730-4f66-8dd5-6a7006db1d76
📒 Files selected for processing (10)
deps/cloudxr/webxr_client/src/App.tsxdeps/cloudxr/webxr_client/src/CloudXR2DUI.tsxdeps/cloudxr/webxr_client/src/CloudXRUI.tsxdeps/cloudxr/webxr_client/src/RecorderComponent.tsxdeps/cloudxr/webxr_client/src/RecorderContext.tsxdeps/cloudxr/webxr_client/src/TraceVisualization.tsxdeps/cloudxr/webxr_client/src/config/params.tsdeps/cloudxr/webxr_client/src/config/resolve.test.tsdeps/cloudxr/webxr_client/src/index.htmldeps/cloudxr/webxr_client/src/xrInputRecorder.ts
- Wire Load button: destructure onLoadRecording in AppContent, add prop to CloudXRUIProps, render Load button in the recording panel alongside Save - Remove dead ?? true in RecorderComponent: session?.visibilityState comparison always yields boolean; nullish fallback never triggered - Distinct wrist trace colors: rightWrist was same #ff4422 as leftWrist; changed to #ff44cc (magenta) so left/right are visually distinct - Hide trace dots on tracking loss: else if (!showTrace) left stale dots visible when pose dropped out; changed to unconditional else - Gate recording on connected: recording branch lacked the connected guard that replay already had; now frames are only captured when connected=true - importJSON frames validation: only version was checked; malformed payloads without a frames array now throw before reaching the render loop - Replay targetRaySpace: _onGetPose replay branch only handled gripSpace; aim poses now return recorded leftAim/rightAim during replay Skipped: escape >= in index.html — > in HTML5 text content does not require escaping; placeholder attrs and config-text div are consistently valid. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: yanziz-nv <yanziz@nvidia.com>
Load requires a file picker which must be triggered before entering XR. Removed Load from the in-XR dashboard (CloudXRUI) and added a "Load Recording..." button to the Recording Controls section of the 2D settings panel (index.html), wired via a useEffect in AppContent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: yanziz-nv <yanziz@nvidia.com>
There was a problem hiding this comment.
Reviewed by yanziz-reviewer-bot
Summary
Adds an XR input recorder/replayer (monkey-patching XRFrame.prototype.getPose, getJointPose, and XRSession.inputSources) and a rolling 500-point hand/controller path-trace visualization, wired through a new RecorderProvider context and two opt-in 2D-panel dropdowns. All CI checks pass; no new TypeScript errors per npx tsc --noEmit.
Legend: 🚫 Blocker · 💡 Suggestion · 🔍 Nit
| Finding | |
|---|---|
| 🚫 | None |
| 💡 | deps/cloudxr/webxr_client/src/TraceVisualization.tsx:3376 — The BufferGeometry, PointsMaterial, and Points objects produced by makeTraceChannel are never disposed. R3F's <primitive> wrapper does not call .dispose() on unmount; GPU VBOs and shader programs leak until page reload. Add a useEffect that returns a cleanup calling ch.points.geometry.dispose(); (ch.points.material as PointsMaterial).dispose() over all four channels in traceRef.current. |
| 🔍 | deps/cloudxr/webxr_client/src/CloudXRUI.tsx:2405 — showTrace is declared in CloudXRUIProps and destructured with a default value but is never referenced anywhere in CloudXR3DUI's render tree. The actual trace toggle is consumed entirely by TraceVisualization. Remove it from the interface and destructuring to keep the prop surface honest.deps/cloudxr/webxr_client/src/xrInputRecorder.ts:7027 — importJSON validates only r.version === 1 and Array.isArray(r.frames). A corrupt JSON file with frames missing the poses or gamepads keys will be accepted silently and throw a TypeError during replay when _currentReplay.poses.leftGrip is read. Add a minimal shape check on at least r.frames[0]?.poses so corruption surfaces at load time with a useful message rather than mid-replay. |
Actionables (for bots — copy-paste-ready for AI)
Fix if it makes sense in context — these are agent-generated suggestions, not human-vetted obligations. Skip anything that's wrong, already addressed, or not worth the churn.
deps/cloudxr/webxr_client/src/TraceVisualization.tsx:3376— Add auseEffectthat returns a cleanup calling.geometry.dispose()and(ch.points.material as PointsMaterial).dispose()over all fourtraceRef.currentchannels to prevent GPU buffer leaks on unmount.deps/cloudxr/webxr_client/src/CloudXRUI.tsx:2405— Remove the unusedshowTraceprop from theCloudXRUIPropsinterface and fromCloudXR3DUI's destructured parameter list; it is never read in the render output.deps/cloudxr/webxr_client/src/xrInputRecorder.ts:7027— After theArray.isArray(r.frames)check inimportJSON, add a guard validatingr.frames[0]?.posesandr.frames[0]?.gamepadsexist so malformed recordings throw an actionable error at load time rather than a crypticTypeErrorduring replay.
…play OOB - TraceVisualization: only hide dots when !showTrace; during tracking loss (showTrace && !pose) leave accumulated history visible rather than clearing - TraceVisualization: remove unused HAND_JOINT_COUNT constant - RecorderContext: move file input creation into a useEffect with cleanup (document.body.removeChild) to prevent DOM leak on unmount - index.html / App.tsx: add "Stop Replay" button to 2D panel alongside "Load Recording"; during replay the in-XR pointer follows recorded targetRaySpace poses so the dashboard is uninteractable — the 2D panel button provides an out-of-band escape hatch Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: yanziz-nv <yanziz@nvidia.com>
… interaction During replay _onGetPose was intercepting targetRaySpace and returning recorded aim poses, causing the in-XR UI pointer to follow the recording instead of the user's real hand — making the dashboard uninteractable. Remove the targetRaySpace interception from the replay branch so only gripSpace poses are replayed; targetRaySpace falls through to the real XRFrame.getPose, preserving pointer interaction in the XR dashboard. Also remove the "Stop Replay" 2D panel button added as a workaround; it's no longer needed now that the dashboard is interactable during replay. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: yanziz-nv <yanziz@nvidia.com>
sgrizan-nv
left a comment
There was a problem hiding this comment.
These are nice features, thanks!
The null-check guard let the proactive scene-space capture (using renderer.xr.getReferenceSpace(), which CloudXR may offset) take priority over the actual getPose() caller's baseSpace. During replay, @react-three/xr received poses in CloudXR's offset space and interpreted them as local-floor coordinates, causing controller models to jump to the wrong position. Drop the null-check so _onGetPose always overwrites with the caller's actual reference space (local-floor). The proactive capture in beginFrame still runs as a fallback for frames where no caller requests the grip/aim space directly. Storing in local-floor decouples the recording from CloudXR's coordinate transform, making replays portable across sessions with different calibrations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Every recorded frame now carries a required `t` (predictedDisplayTime, ms) and `head` (viewer pose in scene ref space, null when unavailable), plus a `recordedAt` epoch on the recording. These are captured only; replay still advances one frame per tick and does not read them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Motivation
During IsaacTeleop sessions it is hard to diagnose whether hand/controller poses are drifting, latency is accumulating, or whether a bug is in the live path vs. the recorded replay path. This PR adds two troubleshooting tools — a rolling path-trace visualization and an XR input recorder — to make these problems visible and reproducible without needing a hardware setup to repro.
Changes
New files
xrInputRecorder.ts— frame-indexed recorder; monkey-patchesXRFrame.getPose,getJointPose, andXRSession.inputSourcesto capture and replay grip/aim poses, hand joint arrays, and gamepad button/axis state; JSON save/loadRecorderContext.tsx/RecorderComponent.tsx— React context + null-rendering R3F component that drivesrecorder.beginFrame()at priority -1001 (before CloudXR SDK at -1000); live frame count forwarded every 30 frames viaonFrameRecordTraceVisualization.tsx— rolling 500-point path trace (6 px screen-space dots,sizeAttenuation:falsefor WebXR stereo) for grip and wrist; queries live XR poses in idle,recorder.currentFrameduring recording/replayModified files
index.html/CloudXR2DUI.tsx— adds Hand/Controller Trace select (In-XR Comfort group) and Recording Controls select (Troubleshooting group); both default hidden, persisted to localStorage, URL-param overrideable (showTraceInXR,showRecordingControls)CloudXRUI.tsx— conditional Recording section (Record/Stop, Play/Stop, Save, Load) in the in-XR overlay, shown only whenshowRecordingControlsis enabledApp.tsx— wrapsAppContentinRecorderProvider; mountsRecorderComponentandTraceVisualizationin the R3F canvasparams.ts/resolve.test.ts— registersshowTraceInXRandshowRecordingControlsURL paramsHow to try it
Path trace
Record → Save → Replay
isaacteleop-input-recording.jsonTesting
isaacteleop-input-recording.jsondownloadsnpx tsc --noEmit— no new type errorsnpm test(jest) — all tests pass🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
UI Improvements