With an explicit layout (Pipeline Layouts), each binding the entry point uses must exist in the layout, be visible to its stage, match its type and access mode, and have a minBindingSize no smaller than the WGSL type. A sale shader (read prices at binding 0, read_write output at 1, a vec2f uniform at 2) met six layouts in error scopes:
| Layout change | Chrome 154 1 's message ("..." marks a cut) |
|---|---|
| Binding 2 as 'storage' | The buffer type in the shader (BufferBindingType::Uniform) is not compatible ... |
| Binding 1 visible to FRAGMENT only | Entry point's stage (ShaderStage::Compute) is not in the binding visibility ... |
| Binding 2 minBindingSize: 4 | The shader uses more bytes of the buffer (8) than the layout's minBindingSize (4). |
| Binding 1 left out | Binding doesn't exist in [BindGroupLayoutInternal "sale"]. |
The correct layout passed, and so did one with an extra binding 3: a layout may offer more than a shader uses. Access modes must match exactly; storage for a read variable failed too.
<!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="390"></canvas>
<canvas id="labels" width="600" height="390"></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 saleShader = /* wgsl */ `
@group(0) @binding(0) var<storage, read> prices: array<f32>;
@group(0) @binding(1) var<storage, read_write> sale: array<f32>;
@group(0) @binding(2) var<uniform> rule: vec2f; // percent off, minimum price
@compute @workgroup_size(6) fn main(@builtin(local_invocation_index) i: u32) {
sale[i] = max(prices[i] * (1 - rule.x / 100), rule.y);
}`;
const C = GPUShaderStage.COMPUTE, F = GPUShaderStage.FRAGMENT;
const good = [{ binding: 0, visibility: C, buffer: { type: 'read-only-storage' } },
{ binding: 1, visibility: C, buffer: { type: 'storage' } },
{ binding: 2, visibility: C, buffer: { type: 'uniform', minBindingSize: 8 } }];
const variant = (i, entry) => good.map((e, k) => (k === i ? entry : e)).filter(Boolean);
const layouts = [
['The correct layout', good],
['Binding 2 as storage', variant(2, { binding: 2, visibility: C, buffer: { type: 'storage' } })],
['Binding 1 visible to FRAGMENT only', variant(1, { binding: 1, visibility: F, buffer: { type: 'storage' } })],
['Binding 2 minBindingSize: 4', variant(2, { binding: 2, visibility: C, buffer: { type: 'uniform', minBindingSize: 4 } })],
['Binding 1 left out', variant(1, null)],
['An extra, unused binding 3', [...good, { binding: 3, visibility: C, buffer: { type: 'uniform' } }]]];
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({ code: saleShader });
const results = [];
let working = null;
for (const [name, entries] of layouts) {
device.pushErrorScope('validation');
const layout = device.createBindGroupLayout({ label: 'sale', entries });
const pipeline = device.createComputePipeline({ layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
compute: { module } });
const error = await device.popErrorScope();
results.push([name, error]);
if (!error && !working) working = { layout, pipeline };
}
// Run the sale with the first layout that passed, then draw prices and sale prices.
const B = GPUBufferUsage;
const prices = device.createBuffer({ size: 24, usage: B.STORAGE | B.COPY_DST });
device.queue.writeBuffer(prices, 0, new Float32Array([14.99, 39.50, 24.00, 18.75, 16.20, 21.30]));
const sale = device.createBuffer({ size: 24, usage: B.STORAGE });
const rule = device.createBuffer({ size: 8, usage: B.UNIFORM | B.COPY_DST });
device.queue.writeBuffer(rule, 0, new Float32Array([30, 12])); // 30% off, never below $12
const bars = device.createShaderModule({ code: `
@group(0) @binding(0) var<storage> prices: array<f32>;
@group(0) @binding(1) var<storage> sale: array<f32>;
struct Out { @builtin(position) pos: vec4f, @location(0) color: vec3f }
@vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) i: u32) -> Out {
let q = vec2f(f32(v & 1), f32(v >> 1));
let book = i / 2; let isSale = i % 2 == 1;
let h = select(prices[book], sale[book], isSale) / 40;
let x = -0.9 + f32(book) * 0.3 + select(0.0, 0.11, isSale);
return Out(vec4f(x + q.x * 0.1, -0.95 + q.y * h * 0.5, 0, 1), select(vec3f(0.72, 0.70, 0.66), vec3f(0.16, 0.56, 0.30), isSale));
}
@fragment fn fs(in: Out) -> @location(0) vec4f { return vec4f(in.color, 1); }` });
const render = device.createRenderPipeline({ layout: 'auto', primitive: { topology: 'triangle-strip' },
vertex: { module: bars }, fragment: { module: bars, targets: [{ format }] } });
const encoder = device.createCommandEncoder();
const cp = encoder.beginComputePass();
cp.setPipeline(working.pipeline);
cp.setBindGroup(0, device.createBindGroup({ layout: working.layout, entries: [
{ binding: 0, resource: { buffer: prices } }, { binding: 1, resource: { buffer: sale } }, { binding: 2, resource: { buffer: rule } }] }));
cp.dispatchWorkgroups(1);
cp.end();
const pass = encoder.beginRenderPass({ colorAttachments: [{ view: context.getCurrentTexture().createView(),
clearValue: [0.97, 0.96, 0.93, 1], loadOp: 'clear', storeOp: 'store' }] });
pass.setPipeline(render);
pass.setBindGroup(0, device.createBindGroup({ layout: render.getBindGroupLayout(0), entries: [
{ binding: 0, resource: { buffer: prices } }, { binding: 1, resource: { buffer: sale } }] }));
pass.draw(4, 12);
pass.end();
device.queue.submit([encoder.finish()]);
results.forEach(([name, error], i) => {
const y = 6 + i * 44;
ink.fillStyle = error ? '#fbe3dd' : '#e2f1e5'; ink.beginPath(); ink.roundRect(8, y, 584, 40, 8); ink.fill();
ink.fillStyle = error ? '#b03a28' : '#2a8f4d'; ink.font = 'bold 13px system-ui, sans-serif';
ink.fillText(`${error ? 'fails' : 'passes'}: ${name}`, 18, y + 17);
ink.fillStyle = '#444'; ink.font = '11px ui-monospace, monospace';
ink.fillText(error ? error.message.split('\n')[0].slice(0, 86) : 'createComputePipeline() succeeded', 18, y + 33);
});
ink.fillStyle = '#333'; ink.font = '12px system-ui, sans-serif';
ink.fillText('Run with the correct layout: grey price, green sale (30% off, at least $12)', 8, 282);
}
main();
</script>