9.0 KiB
WebGPU Device Loss Handling
What Is Device Loss?
Device loss occurs when the GPU driver cannot continue processing commands. Causes include:
- Driver crashes
- Extreme resource pressure
- Long-running shaders (GPU watchdog triggers after ~10 seconds)
- Driver updates
- Significant device configuration changes
When a device is lost, the GPUDevice object and all objects created with it become unusable. All buffers, textures, pipelines, and GPU memory are discarded.
Listening for Device Loss
Detect loss by attaching a callback to the device's lost promise:
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) { return; }
const device = await adapter.requestDevice();
device.lost.then((info) => {
console.error('WebGPU device lost:', info.message);
// Handle recovery
});
Important: Don't await this promise directly - it will block indefinitely if loss never occurs.
Device Loss Information
The GPUDeviceLostInfo object provides:
| Property | Description |
|---|---|
reason |
'destroyed' (intentional via destroy()) or 'unknown' (unexpected) |
message |
Human-readable debugging info (don't parse programmatically) |
device.lost.then((info) => {
if (info.reason === 'unknown') {
// Unexpected loss - attempt recovery
handleUnexpectedDeviceLoss();
} else {
// Intentional destruction - expected behavior
}
});
Devices Starting Lost
adapter.requestDevice() always returns a GPUDevice, but it may already be lost if creation failed. This occurs when the adapter was "consumed" (used previously) or "expired."
Best practice: Always get a new adapter right before requesting a device.
Recovery Strategies
Minimal Recovery (Page Reload)
For simple applications:
device.lost.then((info) => {
if (info.reason === 'unknown') {
// Warn user before reload
alert('Graphics error occurred. The page will reload.');
location.reload();
}
});
Restart GPU Content Only (Recommended for Three.js)
Recreate the device and reconfigure the canvas without full page reload:
import * as THREE from 'three/webgpu';
let renderer;
let scene, camera;
async function initWebGPU() {
renderer = new THREE.WebGPURenderer();
await renderer.init();
// Access the underlying WebGPU device
const device = renderer.backend.device;
device.lost.then((info) => {
console.error('Device lost:', info.message);
if (info.reason === 'unknown') {
// Dispose current renderer
renderer.dispose();
// Reinitialize
initWebGPU();
}
});
// Configure canvas
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Recreate scene content
setupScene();
}
function setupScene() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
// ... add meshes, lights, etc.
}
initWebGPU();
Restore with Application State
For applications with user progress or configuration:
let appState = {
cameraPosition: { x: 0, y: 5, z: 10 },
settings: {},
// Don't save transient data like particle positions
};
// Save state periodically
function saveState() {
appState.cameraPosition = {
x: camera.position.x,
y: camera.position.y,
z: camera.position.z
};
localStorage.setItem('appState', JSON.stringify(appState));
}
// Restore on recovery
async function initWebGPU() {
renderer = new THREE.WebGPURenderer();
await renderer.init();
const savedState = localStorage.getItem('appState');
if (savedState) {
appState = JSON.parse(savedState);
}
setupScene();
// Restore camera position
camera.position.set(
appState.cameraPosition.x,
appState.cameraPosition.y,
appState.cameraPosition.z
);
renderer.backend.device.lost.then((info) => {
if (info.reason === 'unknown') {
saveState();
renderer.dispose();
initWebGPU();
}
});
}
When Recovery Fails
If requestAdapter() returns null after device loss, the OS or browser has blocked GPU access:
async function initWebGPU() {
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
// Check if this is initial failure or post-loss failure
if (hadPreviousDevice) {
showMessage('GPU access lost. Please restart your browser.');
} else {
showMessage('WebGPU is not supported on this device.');
}
return;
}
// Continue with device creation...
}
Testing Device Loss
Using destroy()
Call device.destroy() to simulate loss:
let simulatedLoss = false;
function simulateDeviceLoss() {
simulatedLoss = true;
renderer.backend.device.destroy();
}
// In your device.lost handler:
device.lost.then((info) => {
if (info.reason === 'unknown' || simulatedLoss) {
simulatedLoss = false;
// Treat as unexpected loss for testing
handleDeviceLoss();
}
});
// Add debug keybinding
window.addEventListener('keydown', (e) => {
if (e.key === 'L' && e.ctrlKey && e.shiftKey) {
simulateDeviceLoss();
}
});
Limitations of destroy():
- Unmaps buffers immediately (real loss doesn't)
- Always allows device recovery (real loss may not)
Chrome GPU Process Crash Testing
Navigate to about:gpucrash in a separate tab to crash the GPU process.
Chrome enforces escalating restrictions:
| Crash | Effect |
|---|---|
| 1st | New adapters allowed |
| 2nd within 2 min | Adapter requests fail (resets on page refresh) |
| 3rd within 2 min | All pages blocked (reset after 2 min or browser restart) |
| 3-6 within 5 min | GPU process stops restarting; browser restart required |
Chrome Testing Flags
Bypass crash limits for development:
# macOS
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--disable-domain-blocking-for-3d-apis \
--disable-gpu-process-crash-limit
# Windows
chrome.exe --disable-domain-blocking-for-3d-apis --disable-gpu-process-crash-limit
# Linux
google-chrome --disable-domain-blocking-for-3d-apis --disable-gpu-process-crash-limit
Complete Example
import * as THREE from 'three/webgpu';
import { color, time, oscSine } from 'three/tsl';
let renderer, scene, camera, mesh;
let hadPreviousDevice = false;
async function init() {
// Check WebGPU support
if (!navigator.gpu) {
showError('WebGPU not supported');
return;
}
// Create renderer
renderer = new THREE.WebGPURenderer({ antialias: true });
try {
await renderer.init();
} catch (e) {
if (hadPreviousDevice) {
showError('GPU recovery failed. Please restart browser.');
} else {
showError('Failed to initialize WebGPU.');
}
return;
}
hadPreviousDevice = true;
// Setup device loss handler
const device = renderer.backend.device;
device.lost.then(handleDeviceLoss);
// Setup scene
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshStandardNodeMaterial();
material.colorNode = color(0x00ff00).mul(oscSine(time));
mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 5, 5);
scene.add(light);
scene.add(new THREE.AmbientLight(0x404040));
animate();
}
function handleDeviceLoss(info) {
console.error('Device lost:', info.reason, info.message);
if (info.reason === 'unknown') {
// Cleanup
if (renderer) {
renderer.domElement.remove();
renderer.dispose();
}
// Attempt recovery after short delay
setTimeout(() => {
init();
}, 100);
}
}
function animate() {
if (!renderer) return;
requestAnimationFrame(animate);
mesh.rotation.x += 0.01;
mesh.rotation.y += 0.01;
renderer.render(scene, camera);
}
function showError(message) {
const div = document.createElement('div');
div.textContent = message;
div.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);padding:20px;background:#f44;color:#fff;border-radius:8px;';
document.body.appendChild(div);
}
init();
Best Practices
- Always listen for device loss - Even if you just show an error message
- Get a fresh adapter before each device request - The GPU hardware may have changed
- Don't parse the message field - It's implementation-specific and changes between browsers
- Save critical application state - Restore user progress after recovery
- Don't save transient state - Particle positions, physics state can be reset
- Test your recovery path - Use
destroy()and Chrome'sabout:gpucrash - Handle adapter failure gracefully - Distinguish between initial failure and post-loss failure
- Add a short delay before recovery - Give the system time to stabilize