A shader that "draws nothing" cannot be stepped through, so work from the outside in:
Read every compilation message (getCompilationInfo(), getCompilationInfo()).
Catch pipeline and bind group validation errors with error scopes (Errors and Device Loss). uncapturederror fires only after something is submitted or awaited, so end test pages with await device.queue.onSubmittedWorkDone().
Check bindings against an explicit layout (Matching Layouts) and byte layouts with a read-back (Shared Book Struct).
Return a flat color with culling off, to prove the vertices land in clip space.
Return the doubted value as a color (vec4f(uv, 0, 1), vec4f(normal * 0.5 + 0.5, 1)), or write it to a storage buffer in compute.
Capture the frame in WebGPU Inspector 369 (DevTools Debugging).
Shrink the problem to one triangle or one workgroup, then add pieces back, and label every descriptor, since Chrome 1 quotes labels in its messages.
<!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="220"></canvas>
<canvas id="labels" width="600" height="220"></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);
}
// A book-shaped box generated from vertex_index (36 vertices), drawn four times;
// instance_index picks the panel and what the fragment shader outputs.
const code = /* wgsl */ `
@group(0) @binding(0) var<uniform> time: f32;
struct Out { @builtin(position) pos: vec4f, @location(0) uv: vec2f, @location(1) normal: vec3f,
@location(2) @interpolate(flat) mode: u32 }
@vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) mode: u32) -> Out {
let face = v / 6;
let q = array(vec2f(0, 0), vec2f(1, 0), vec2f(1, 1), vec2f(0, 0), vec2f(1, 1), vec2f(0, 1))[v % 6];
let axes = array(vec3f(1, 0, 0), vec3f(-1, 0, 0), vec3f(0, 1, 0), vec3f(0, -1, 0), vec3f(0, 0, 1), vec3f(0, 0, -1));
let n = axes[face];
let u = select(vec3f(0, 0, n.x), vec3f(-n.z, 0, 0), abs(n.x) < 0.5); // tangent
let u2 = select(u, vec3f(1, 0, 0), abs(n.y) > 0.5);
let w = cross(n, u2);
let size = vec3f(0.7, 1.0, 0.22); // a book: wide, tall, thin
var p = (n * 0.5 + u2 * (q.x - 0.5) + w * (q.y - 0.5)) * size;
let a = time; let b = 0.4;
p = vec3f(p.x * cos(a) + p.z * sin(a), p.y, -p.x * sin(a) + p.z * cos(a)); // spin
p = vec3f(p.x, p.y * cos(b) - p.z * sin(b), p.y * sin(b) + p.z * cos(b)); // tilt
var nn = vec3f(n.x * cos(a) + n.z * sin(a), n.y, -n.x * sin(a) + n.z * cos(a));
nn = vec3f(nn.x, nn.y * cos(b) - nn.z * sin(b), nn.y * sin(b) + nn.z * cos(b));
let z = p.z + 2.2; // simple perspective
let clip = vec4f(p.x * 1.9 / 1.4, p.y * 1.9, z * 0.4 - 0.2, z);
let panel = (f32(mode) - 1.5) * 0.5; // shift each copy into its quarter
return Out(vec4f(clip.x * 0.25 + panel * clip.w, clip.y, clip.z, clip.w), q, nn, mode);
}
@fragment fn fs(in: Out) -> @location(0) vec4f {
switch in.mode {
case 0u: { return vec4f(1, 0, 1, 1); } // 1. flat colour: do vertices land?
case 1u: { return vec4f(in.uv, 0, 1); } // 2. the doubted value as colour
case 2u: { return vec4f(normalize(in.normal) * 0.5 + 0.5, 1); } // 3. normals as colour
default: { // 4. the real shading
let light = max(dot(normalize(in.normal), normalize(vec3f(0.4, 0.6, -0.7))), 0.0);
return vec4f(vec3f(0.08, 0.40, 0.75) * (0.25 + 0.75 * light), 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: 'debug-views.wgsl', code });
const { messages } = await module.getCompilationInfo(); // step 1: read every message
device.pushErrorScope('validation'); // step 2: catch pipeline errors
const pipeline = device.createRenderPipeline({ label: 'debug views', layout: 'auto',
vertex: { module }, fragment: { module, targets: [{ format }] },
primitive: { cullMode: 'none' }, // culling off while debugging
depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' } });
const pipelineError = await device.popErrorScope();
const depth = device.createTexture({ size: [600, 220], format: 'depth24plus', usage: GPUTextureUsage.RENDER_ATTACHMENT });
const time = device.createBuffer({ label: 'time', size: 4, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
const group = device.createBindGroup({ label: 'frame', layout: pipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: time } }] });
ink.font = '12px system-ui, sans-serif'; ink.fillStyle = '#222'; ink.textAlign = 'center';
['flat colour, no culling', 'vec4f(uv, 0, 1)', 'normal * 0.5 + 0.5', 'final shading'].forEach((t, i) => ink.fillText(t, 75 + i * 150, 200));
ink.fillStyle = '#1f4f8a'; ink.textAlign = 'left';
ink.fillText(`compilation messages: ${messages.length}, pipeline error: ${pipelineError ? pipelineError.message : 'none'}`, 10, 18);
function frame(now) {
device.queue.writeBuffer(time, 0, new Float32Array([now / 1200]));
const encoder = device.createCommandEncoder({ label: 'debug frame' });
const pass = encoder.beginRenderPass({ label: 'four views',
colorAttachments: [{ view: context.getCurrentTexture().createView(), clearValue: [0.97, 0.96, 0.93, 1], loadOp: 'clear', storeOp: 'store' }],
depthStencilAttachment: { view: depth.createView(), depthClearValue: 1, depthLoadOp: 'clear', depthStoreOp: 'discard' } });
pass.setPipeline(pipeline);
pass.setBindGroup(0, group);
pass.draw(36, 4); // one box, four debug views
pass.end();
device.queue.submit([encoder.finish()]);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
main();
</script>