Files
2026-03-28 13:57:54 +08:00

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();
  }
});

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

  1. Always listen for device loss - Even if you just show an error message
  2. Get a fresh adapter before each device request - The GPU hardware may have changed
  3. Don't parse the message field - It's implementation-specific and changes between browsers
  4. Save critical application state - Restore user progress after recovery
  5. Don't save transient state - Particle positions, physics state can be reset
  6. Test your recovery path - Use destroy() and Chrome's about:gpucrash
  7. Handle adapter failure gracefully - Distinguish between initial failure and post-loss failure
  8. Add a short delay before recovery - Give the system time to stabilize