DevTools Debugging

Debugging WebGPU Calls in Chrome DevTools

Chrome 1 's DevTools have no WebGPU panel, so debugging happens mostly in the Console, and how useful its messages are is up to you. Every descriptor accepts a label, and encoders accept debug groups:

Labeling a frame so its errors say where they happenedJavaScript
const encoder = device.createCommandEncoder({ label: 'frame 42' });
encoder.pushDebugGroup('draw shelf');
const pass = encoder.beginRenderPass({ label: 'shelf pass', colorAttachments });
pass.draw(6);                                      // bug: no pipeline set
pass.end();
encoder.popDebugGroup();

Run on this machine, the mistake produced "No pipeline set.", then the call chain from innermost to outermost (While encoding [RenderPassEncoder "shelf pass"].Draw(6, 1, 0, 0), While finishing [CommandEncoder "frame 42"]), then a "Debug group stack" naming draw shelf. Unlabeled, the pass reads [RenderPassEncoder (unlabeled)], so label everything. A GPU object in the debugger exposes only a few properties (a buffer's label, size, usage, mapState), never its contents. For more, install WebGPU Inspector 369 (github.com/brendan-duncan/webgpu_inspector (https://github.com/brendan-duncan/webgpu_inspector 369 ), MIT, v1.1.1, from the Chrome Web Store 1 or Firefox Add-ons 102 ). Its DevTools tab lists every live GPU object, captures a frame's commands with pass outputs and buffer contents, edits shaders live and records frames for playback, by injecting a script that wraps the page's WebGPU calls.

Labels and debug groups make a validation message say exactly where a missing pipeline broke the frameHTMLLive
<!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="360"></canvas>
  <canvas id="labels" width="600" height="360"></canvas>
</div>
<script>
const canvas = document.getElementById('view');
const ink = document.getElementById('labels').getContext('2d');

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

const code = /* wgsl */ `
@vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) i: u32) -> @builtin(position) vec4f {
  let q = array(vec2f(0, 0), vec2f(1, 0), vec2f(0, 1), vec2f(1, 0), vec2f(1, 1), vec2f(0, 1))[v];
  return vec4f(-0.92 + f32(i) * 0.31 + q.x * 0.25, 0.4 + q.y * (0.5 - f32(i % 3) * 0.06), 0, 1);
}
@fragment fn fs() -> @location(0) vec4f { return vec4f(0.08, 0.40, 0.75, 1); }`;

async function main() {
  const adapter = await navigator.gpu?.requestAdapter();
  if (!adapter) return showMessage('WebGPU is not available in this browser');
  const device = await adapter.requestDevice();
  const context = canvas.getContext('webgpu');
  const format = navigator.gpu.getPreferredCanvasFormat();
  context.configure({ device, format });
  const module = device.createShaderModule({ label: 'shelf.wgsl', code });
  const pipeline = device.createRenderPipeline({ label: 'shelf pipeline', layout: 'auto',
    vertex: { module }, fragment: { module, targets: [{ format }] } });
  // A fresh canvas texture per frame: it expires once the task that got it ends.
  const colorAttachments = () => [{ view: context.getCurrentTexture().createView(),
    clearValue: [0.97, 0.96, 0.93, 1], loadOp: 'clear', storeOp: 'store' }];

  // Frame 42 has a bug; an error scope catches the message so we can print it.
  device.pushErrorScope('validation');
  const broken = device.createCommandEncoder({ label: 'frame 42' });
  broken.pushDebugGroup('draw shelf');
  const bad = broken.beginRenderPass({ label: 'shelf pass', colorAttachments: colorAttachments() });
  bad.draw(6);                                       // bug: no pipeline set
  bad.end();
  broken.popDebugGroup();
  broken.finish();                                   // validation happens here; not submitted
  const error = await device.popErrorScope();

  // Frame 43 is fixed and labelled the same way.
  const encoder = device.createCommandEncoder({ label: 'frame 43' });
  encoder.pushDebugGroup('draw shelf');
  const pass = encoder.beginRenderPass({ label: 'shelf pass', colorAttachments: colorAttachments() });
  pass.setPipeline(pipeline);
  pass.draw(6, 6);
  pass.end();
  encoder.popDebugGroup();
  device.queue.submit([encoder.finish()]);

  ink.font = 'bold 13px system-ui, sans-serif'; ink.fillStyle = '#222';
  ink.fillText('frame 43 (fixed) drew the shelf above; frame 42 reported:', 14, 128);
  ink.fillStyle = '#fff4f2'; ink.fillRect(10, 140, 580, 210);
  ink.font = '11.5px ui-monospace, monospace'; ink.fillStyle = '#8a2b2b';
  const lines = (error ? error.message : 'no error?').split('\n').flatMap((line) => line.match(/.{1,80}(\s|$)|.{1,80}/g) || ['']);
  lines.slice(0, 11).forEach((line, i) => ink.fillText(line.trim(), 18, 162 + i * 17));
}
main();
</script>