fix(handtracking): reduce GPU pressure on WebGL context loss

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>
This commit is contained in:
Tom Boullay
2026-06-02 16:48:39 +02:00
parent 864e075b42
commit d217c3376b
8 changed files with 156 additions and 35 deletions
+21 -5
View File
@@ -31,11 +31,20 @@ import { CharacterSystem } from "@/world/characters/CharacterSystem";
import { Player } from "@/world/player/Player";
import { TestMap } from "@/world/debug/TestMap";
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
import type { HandTrackingGloveHandedness } from "@/hooks/handTracking/useHandTrackingGloveStatus";
import type { HandTrackingHand } from "@/types/handTracking/handTracking";
interface WorldProps {
onLoadingStateChange?: SceneLoadingChangeHandler | undefined;
}
function hasTrackedHand(
hands: HandTrackingHand[],
handedness: HandTrackingGloveHandedness,
): boolean {
return hands.some((hand) => hand.handedness.toLowerCase() === handedness);
}
export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
useEnvironmentDebug();
useMapPerformanceDebug();
@@ -49,7 +58,7 @@ export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
(state) => state.showPlayerModel,
);
const showDebugOctree = useDebugVisualsStore((state) => state.showOctree);
const { status, usageStatus } = useHandTrackingSnapshot();
const { hands, status, usageStatus } = useHandTrackingSnapshot();
const {
octree,
gameplayReady,
@@ -63,8 +72,11 @@ export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
? PLAYER_SPAWN_POSITION_GAME
: PLAYER_SPAWN_POSITION_PHYSICS;
const showHandTrackingGloves =
sceneMode === "physics" ||
(status !== "idle" && usageStatus !== "inactive");
status === "connected" && usageStatus !== "inactive" && hands.length > 0;
const showLeftHandTrackingGlove =
showHandTrackingGloves && hasTrackedHand(hands, "left");
const showRightHandTrackingGlove =
showHandTrackingGloves && hasTrackedHand(hands, "right");
const spawnPlayer =
cameraMode !== "debug" &&
(sceneMode === "game" ? gameplayReady : octree !== null);
@@ -82,8 +94,12 @@ export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
) : null}
{showHandTrackingGloves ? (
<Suspense fallback={null}>
<HandTrackingGlove handedness="left" />
<HandTrackingGlove handedness="right" />
{showLeftHandTrackingGlove ? (
<HandTrackingGlove handedness="left" />
) : null}
{showRightHandTrackingGlove ? (
<HandTrackingGlove handedness="right" />
) : null}
</Suspense>
) : null}
{cameraMode === "debug" ? <DebugCameraControls /> : null}