Files
three-offshore-vibe/.codex/skills/webgpu-threejs-tsl/docs/device-loss.md
2026-03-28 13:57:54 +08:00

360 lines
9.0 KiB
Markdown

# 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:
```javascript
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) |
```javascript
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:
```javascript
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:
```javascript
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:
```javascript
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:
```javascript
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:
```javascript
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:
```bash
# 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
```javascript
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