10 KiB
GPU Device Loss and Recovery
GPU device loss occurs when the browser's WebGPU device becomes unavailable. Common causes: driver crash, GPU hang, system sleep/wake, GPU reset by OS, resource exhaustion, or the user switching GPUs. Your application must handle this gracefully.
How Device Loss Manifests
When the GPU device is lost:
- All GPU resources (buffers, textures, pipelines) become invalid.
- Subsequent GPU operations silently fail or throw.
- The canvas may go black or freeze on the last rendered frame.
GPUDevice.lostpromise resolves with aGPUDeviceLostInfoobject.
Detection
WebGPU Native API
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
device.lost.then((info) => {
console.error(`GPU device lost: ${info.reason}`, info.message);
if (info.reason === 'destroyed') {
// Intentional -- app called device.destroy()
} else {
// Unexpected loss -- attempt recovery
recoverDevice();
}
});
Three.js WebGPURenderer
Three.js wraps the device internally. You can detect loss by listening on the renderer's underlying device:
import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
async function setupRenderer(canvas: HTMLCanvasElement) {
const renderer = new WebGPURenderer({ canvas, antialias: true });
await renderer.init();
// Access the underlying GPUDevice
const backend = renderer.backend;
const device = backend.device;
device.lost.then((info) => {
console.error('WebGPU device lost:', info.reason, info.message);
handleDeviceLoss(renderer);
});
return renderer;
}
React Error Boundary for Device Loss
Wrap your WebGPU canvas in an error boundary that catches device-loss errors and offers recovery.
DeviceLossErrorBoundary Component
import { Component, type ReactNode } from 'react';
interface DeviceLossState {
hasError: boolean;
error: Error | null;
recovering: boolean;
}
interface DeviceLossProps {
children: ReactNode;
fallback?: ReactNode;
onDeviceLost?: (error: Error) => void;
maxRetries?: number;
}
export class DeviceLossErrorBoundary extends Component<
DeviceLossProps,
DeviceLossState
> {
private retryCount = 0;
constructor(props: DeviceLossProps) {
super(props);
this.state = { hasError: false, error: null, recovering: false };
}
static getDerivedStateFromError(error: Error): Partial<DeviceLossState> {
// Check if the error is GPU-related
const isGPUError =
error.message.includes('device') ||
error.message.includes('GPU') ||
error.message.includes('context lost') ||
error.message.includes('destroyed') ||
error.name === 'GPUDeviceLostError';
if (isGPUError) {
return { hasError: true, error };
}
// Re-throw non-GPU errors
throw error;
}
componentDidCatch(error: Error) {
this.props.onDeviceLost?.(error);
}
handleRetry = () => {
const maxRetries = this.props.maxRetries ?? 3;
if (this.retryCount >= maxRetries) {
return; // Give up after max retries
}
this.retryCount++;
this.setState({ hasError: false, error: null, recovering: true });
// Small delay to let GPU recover
setTimeout(() => {
this.setState({ recovering: false });
}, 1000);
};
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div style={{ padding: 20, textAlign: 'center' }}>
<h3>GPU Connection Lost</h3>
<p>The graphics device became unavailable.</p>
<button onClick={this.handleRetry}>
Retry ({this.retryCount}/{this.props.maxRetries ?? 3})
</button>
</div>
);
}
if (this.state.recovering) {
return <div style={{ padding: 20 }}>Reconnecting to GPU...</div>;
}
return this.props.children;
}
}
Usage in R3F
function App() {
return (
<DeviceLossErrorBoundary
maxRetries={3}
onDeviceLost={(err) => {
analytics.track('gpu_device_lost', { message: err.message });
}}
fallback={
<div>
<p>3D rendering is unavailable.</p>
<p>Try refreshing the page or switching to a different browser.</p>
</div>
}
>
<WebGPUCanvas>
<Scene />
</WebGPUCanvas>
</DeviceLossErrorBoundary>
);
}
Recovery Strategies
Strategy 1: Full Remount (Simple, Reliable)
Unmount and remount the entire Canvas. This destroys all Three.js state and recreates it from scratch.
function RecoverableScene() {
const [key, setKey] = useState(0);
const handleDeviceLoss = useCallback(() => {
// Increment key to force full remount
setTimeout(() => setKey((k) => k + 1), 1000);
}, []);
return (
<DeviceLossErrorBoundary
key={key}
onDeviceLost={handleDeviceLoss}
>
<WebGPUCanvas>
<Scene />
</WebGPUCanvas>
</DeviceLossErrorBoundary>
);
}
Strategy 2: Renderer Recreation (Preserves Scene Graph)
More sophisticated -- recreate only the renderer while keeping the scene graph intact. This requires accessing Three.js internals.
function useDeviceLossRecovery() {
const { gl, set } = useThree();
useEffect(() => {
const renderer = gl as unknown as WebGPURenderer;
const backend = renderer.backend;
if (!backend?.device) return;
backend.device.lost.then(async (info) => {
console.warn('GPU device lost, attempting recovery...');
// Create a new renderer on the same canvas
const canvas = renderer.domElement;
const newRenderer = new WebGPURenderer({
canvas,
antialias: true,
});
await newRenderer.init();
// Replace the renderer in R3F state
set({ gl: newRenderer as unknown as THREE.WebGLRenderer });
});
}, [gl, set]);
}
Strategy 3: Graceful Degradation to WebGL
Fall back to WebGL when WebGPU fails:
async function createRendererWithFallback(canvas: HTMLCanvasElement) {
// Try WebGPU first
if (navigator.gpu) {
try {
const renderer = new WebGPURenderer({ canvas, antialias: true });
await renderer.init();
return { renderer, backend: 'webgpu' as const };
} catch {
console.warn('WebGPU init failed, falling back to WebGL');
}
}
// WebGL fallback
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
return { renderer, backend: 'webgl' as const };
}
State Preservation
When recovering from device loss, you may want to preserve application state (camera position, animation progress, user selections). Store critical state outside the Three.js scene graph.
interface SceneState {
cameraPosition: [number, number, number];
cameraTarget: [number, number, number];
animationProgress: number;
selectedObjectId: string | null;
}
function useSceneStateBackup() {
const stateRef = useRef<SceneState>({
cameraPosition: [0, 5, 10],
cameraTarget: [0, 0, 0],
animationProgress: 0,
selectedObjectId: null,
});
// Periodically snapshot camera state
useFrame(({ camera }) => {
stateRef.current.cameraPosition = camera.position.toArray();
});
return stateRef;
}
After recovery, restore from the backup:
function Scene({ initialState }: { initialState?: SceneState }) {
const cameraRef = useRef<THREE.PerspectiveCamera>(null!);
useEffect(() => {
if (initialState) {
cameraRef.current.position.fromArray(initialState.cameraPosition);
}
}, [initialState]);
return (
<perspectiveCamera ref={cameraRef} />
);
}
Compute Shader Buffer Recovery
Storage buffers for compute shaders are lost along with the device. If the simulation state is important, periodically snapshot it:
function useComputeStateBackup(
renderer: WebGPURenderer,
storageBuffer: StorageBuffer,
intervalMs = 5000
) {
const backupRef = useRef<Float32Array | null>(null);
useEffect(() => {
const interval = setInterval(async () => {
try {
const data = await renderer.getArrayBufferAsync(storageBuffer.value);
backupRef.current = new Float32Array(data);
} catch {
// Device may already be lost
}
}, intervalMs);
return () => clearInterval(interval);
}, [renderer, storageBuffer, intervalMs]);
return backupRef;
}
Testing Device Loss
Simulating Device Loss in Tests
// In a Vitest test
describe('device loss recovery', () => {
it('should remount canvas on device loss', async () => {
const onDeviceLost = vi.fn();
const { rerender } = render(
<DeviceLossErrorBoundary onDeviceLost={onDeviceLost}>
<WebGPUCanvas>
<Scene />
</WebGPUCanvas>
</DeviceLossErrorBoundary>
);
// Simulate device loss by triggering error boundary
// In a real scenario, the GPU device.lost promise resolves
// For testing, throw a simulated GPU error
});
});
Chrome DevTools
- Open Chrome DevTools.
- Go to
chrome://gputo see GPU status. - Use
chrome://flags/#force-gpu-rasterizationfor GPU stress testing. - The WebGPU spec provides
device.destroy()for testing intentional loss.
Programmatic Device Destruction
// Intentionally destroy device for testing recovery
function testDeviceLoss(renderer: WebGPURenderer) {
const device = renderer.backend.device;
device.destroy(); // Triggers device.lost promise
}
Integration with @suaveplan/r3f-performance
The @suaveplan/r3f-performance package monitors frame timing and GPU
utilization. On device loss, the performance monitor should:
- Detect the anomaly (sudden drop to 0 FPS or error events).
- Emit a
'device-lost'event via the performance event bus. - Pause metric collection during recovery.
- Resume collection after the new device is initialized.
- Log a recovery marker in the performance timeline.
// Performance monitor integration
performanceMonitor.on('device-lost', () => {
performanceMonitor.pause();
});
performanceMonitor.on('device-recovered', () => {
performanceMonitor.resume();
performanceMonitor.addMarker('gpu-recovery');
});
Checklist
When implementing WebGPU in a Suaveplan R3F component:
- Wrap the Canvas in
DeviceLossErrorBoundary - Provide meaningful fallback UI
- Preserve important scene state outside the GPU
- Test with
device.destroy()during development - Log device loss events to analytics
- Consider WebGL fallback for critical features
- Handle async renderer init errors
- Clean up GPU resources in React effect cleanup functions