Device Loss Recovery

Detecting and Recovering from Device Loss

A device is lost when the driver crashes or updates, the GPU resets after an overlong shader, the browser reclaims resources, or a laptop switches power mode. device.lost is a promise that resolves, never rejects, with a GPUDeviceLostInfo whose reason is 'destroyed' (you called device.destroy()) or 'unknown' (anything else), plus a message for logs. device.destroy() is the only way to cause a loss on demand, so a test page on this machine destroyed its device and kept using it, with three results:

Every buffer, pipeline and bind group, and the canvas configuration, must then be rebuilt.

A spinning cover that survives device loss: device.lost resolves, and everything is rebuilt from a fresh adapterHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f4ee; font: 14px system-ui, sans-serif; }
  .stage { position: relative; width: 100%; max-width: 600px; }
  .stage canvas { display: block; width: 100%; }
  .stage canvas + canvas { position: absolute; inset: 0; pointer-events: none; }
</style>
<div class="stage">
  <canvas id="view" width="600" height="340"></canvas>
  <canvas id="labels" width="600" height="340"></canvas>
</div>
<script>
const canvas = document.getElementById('view');
const ink = document.getElementById('labels').getContext('2d');
const log = [];

function showMessage(text) {                     // 2D fallback when WebGPU is missing
  const ctx = canvas.getContext('2d');
  ctx.fillStyle = '#fbeaea'; ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = '#8a2b2b'; ctx.font = '18px system-ui, sans-serif'; ctx.textAlign = 'center';
  ctx.fillText(text, canvas.width / 2, canvas.height / 2);
}
function note(line) {
  log.push(`${(performance.now() / 1000).toFixed(1)} s  ${line}`);
  ink.clearRect(0, 0, 600, 340);
  ink.font = '12px ui-monospace, monospace'; ink.fillStyle = '#333';
  log.slice(-12).forEach((l, i) => ink.fillText(l, 300, 40 + i * 20));
}

const code = /* wgsl */ `
@group(0) @binding(0) var<uniform> angle: f32;
struct Out { @builtin(position) pos: vec4f, @location(0) uv: vec2f }
@vertex fn vs(@builtin(vertex_index) v: u32) -> Out {
  let q = vec2f(f32(v & 1), f32(v >> 1)) - 0.5;
  let p = vec2f(q.x * 0.5 * cos(angle), q.y * 0.75);     // a cover turning on its spine
  return Out(vec4f(p.x - 0.45, p.y, 0.5 + 0.2 * q.x * sin(angle), 1), q + 0.5);
}
@fragment fn fs(in: Out) -> @location(0) vec4f {
  let band = step(0.62, in.uv.y) * step(in.uv.y, 0.75);
  return vec4f(mix(vec3f(0.08, 0.40, 0.75), vec3f(0.98, 0.85, 0.40), band), 1);
}`;

let generation = 0;
async function start() {
  const adapter = await navigator.gpu?.requestAdapter();   // recovery starts here, every time
  if (!adapter) return showMessage('WebGPU is not available in this browser');
  const device = await adapter.requestDevice();
  const mine = ++generation;
  note(`device #${mine} created`);
  device.lost.then((info) => {                     // resolves, never rejects
    note(`lost: reason '${info.reason}'`);
    note('rebuilding every GPU object');
    start();                                       // nothing from the old device is reused
  });

  const context = canvas.getContext('webgpu');
  const format = navigator.gpu.getPreferredCanvasFormat();
  context.configure({ device, format });           // the canvas must be reconfigured too
  const module = device.createShaderModule({ code });
  const pipeline = device.createRenderPipeline({ layout: 'auto', primitive: { topology: 'triangle-strip' },
    vertex: { module }, fragment: { module, targets: [{ format }] } });
  const uniform = device.createBuffer({ size: 4, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
  const group = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: uniform } }] });

  const born = performance.now();
  function frame(now) {
    if (mine !== generation) return;               // an old loop stops when its device is gone
    if (now - born > 2500) { note('calling device.destroy()'); device.destroy(); return; }
    device.queue.writeBuffer(uniform, 0, new Float32Array([now / 600]));
    const encoder = device.createCommandEncoder();
    const pass = encoder.beginRenderPass({ colorAttachments: [{ view: context.getCurrentTexture().createView(),
      clearValue: [0.97, 0.96, 0.93, 1], loadOp: 'clear', storeOp: 'store' }] });
    pass.setPipeline(pipeline);
    pass.setBindGroup(0, group);
    pass.draw(4);
    pass.end();
    device.queue.submit([encoder.finish()]);
    requestAnimationFrame(frame);
  }
  requestAnimationFrame(frame);
}
start();
</script>