In dev, <StrictMode> intentionally mounts → unmounts → remounts each
effect to surface non-idempotent code. The hand tracking hooks were
calling getUserMedia and creating MediaPipe / WebSocket runtimes on
every mount, which in practice ran the full start/stop/start cycle
inside a few milliseconds and pushed WebGL over its limit on top
of the loaded scene → context lost.
Add HAND_TRACKING_RUNTIME_START_DELAY_MS (80ms) and delay the actual
start() call behind a setTimeout in both useBrowserHandTracking and
useRemoteHandTracking. The cleanup clears the timer, so a fast
mount/unmount never reaches start(). 80ms is invisible to the user
(<5 frames at 60fps) and also absorbs rapid `nearby` toggles at
trigger borders.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Several mitigations against the WebGL context lost that fires when
hand tracking starts on a loaded scene:
- Canvas: fixed DPR [1,1], antialias off, scoped id="game-canvas",
context-lost handler releases MediaPipe and logs GPU memory counters
- optimizeGLTFScene: cap anisotropy at 2 and stop forcing mipmaps /
needsUpdate on every pass — avoids massive texture re-uploads
- MediaPipe: force CPU delegate (HAND_TRACKING_BROWSER_DELEGATE),
cache the landmarker instance, and expose releaseBrowserHandLandmarker
- useBrowserHandTracking / useRemoteHandTracking: idempotent cleanup
guarded by a cleanedUp flag, try/catch around the detect loop, and
release of the landmarker on stop
- World: mount HandTrackingGlove only when the matching hand is
actually present in the snapshot (status connected + hands.length > 0)
- HandTrackingGlove: drop the eager useGLTF.preload that was running
at startup whether or not hand tracking was used
Does not yet absorb the React StrictMode double-mount — that is the
follow-up commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>