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:
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.
<!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>