layout: 'auto' derives the layout from the shader, and pipeline.getBindGroupLayout(n) returns it. It suits experiments but has three traps, each hit on this machine with one module holding two compute entry points, a and b, that use binding 0 and declare an unused uniform at binding 1:
Unused bindings vanish. A group for a with both bindings failed: "In entries[1], binding index 1 not present in the bind group layout".
Auto layouts belong to one pipeline. A group made for a and used with b failed: "... was created with a default layout, and is not compatible with the [BindGroup] ...", although the two layouts are identical.
Sizes follow the shader: minBindingSize changes with every struct edit.
One explicit layout let both pipelines share a group. Keep 'auto' for single-pipeline jobs and demos.
<!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);
}
// Two entry points use binding 0; binding 1 is declared but used by neither.
const code = /* wgsl */ `
@group(0) @binding(0) var<storage, read_write> data: array<f32>;
@group(0) @binding(1) var<uniform> unused: f32;
@compute @workgroup_size(6) fn a(@builtin(local_invocation_index) i: u32) { data[i] = data[i] * 0.85; } // 15% off
@compute @workgroup_size(6) fn b(@builtin(local_invocation_index) i: u32) { data[i] = round(data[i]) - 0.01; } // x.99 pricing`;
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 });
const B = GPUBufferUsage;
const data = device.createBuffer({ size: 24, usage: B.STORAGE | B.COPY_DST });
device.queue.writeBuffer(data, 0, new Float32Array([14.99, 39.50, 24.00, 18.75, 16.20, 21.30]));
const unused = device.createBuffer({ size: 4, usage: B.UNIFORM });
const report = [];
async function attempt(name, fn) {
device.pushErrorScope('validation');
const value = fn();
const error = await device.popErrorScope();
report.push([name, error]);
return value;
}
// 'auto': each pipeline gets its own layout, derived from what its entry point uses.
const autoA = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'a' } });
const autoB = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'b' } });
await attempt("auto: a group with the declared-but-unused binding 1", () => device.createBindGroup({
layout: autoA.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: data } }, { binding: 1, resource: { buffer: unused } }] }));
const groupA = device.createBindGroup({ layout: autoA.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: data } }] });
await attempt("auto: pipeline a's group used with pipeline b", () => {
const enc = device.createCommandEncoder(), pass = enc.beginComputePass();
pass.setPipeline(autoB); pass.setBindGroup(0, groupA); pass.dispatchWorkgroups(1); pass.end();
return enc.finish();
});
// Explicit: one layout, one group, both pipelines.
const layout = device.createBindGroupLayout({ entries: [
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } }] });
const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [layout] });
const pa = device.createComputePipeline({ layout: pipelineLayout, compute: { module, entryPoint: 'a' } });
const pb = device.createComputePipeline({ layout: pipelineLayout, compute: { module, entryPoint: 'b' } });
const shared = await attempt('explicit: one group shared by pipelines a and b', () => device.createBindGroup({ layout, entries: [
{ binding: 0, resource: { buffer: data } }, { binding: 1, resource: { buffer: unused } }] }));
const bars = device.createShaderModule({ code: `
@group(0) @binding(0) var<storage> data: array<f32>;
@vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) i: u32) -> @builtin(position) vec4f {
let q = vec2f(f32(v & 1), f32(v >> 1));
return vec4f(-0.9 + f32(i) * 0.3 + q.x * 0.22, -0.95 + q.y * data[i] / 40 * 0.75, 0, 1);
}
@fragment fn fs() -> @location(0) vec4f { return vec4f(0.16, 0.56, 0.30, 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.setBindGroup(0, shared); // set once...
cp.setPipeline(pa); cp.dispatchWorkgroups(1); // ...used by a
cp.setPipeline(pb); cp.dispatchWorkgroups(1); // ...and by b
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: data } }] }));
pass.draw(4, 6);
pass.end();
device.queue.submit([encoder.finish()]);
report.forEach(([name, error], i) => {
const y = 8 + i * 58;
ink.fillStyle = error ? '#fbe3dd' : '#e2f1e5'; ink.beginPath(); ink.roundRect(8, y, 584, 52, 8); ink.fill();
ink.fillStyle = error ? '#b03a28' : '#2a8f4d'; ink.font = 'bold 13px system-ui, sans-serif';
ink.fillText(`${error ? 'fails' : 'works'}: ${name}`, 18, y + 20);
ink.fillStyle = '#444'; ink.font = '11px ui-monospace, monospace';
ink.fillText(error ? error.message.split('\n')[0].slice(0, 86) : 'setBindGroup(0, shared) once, then setPipeline(a) and setPipeline(b)', 18, y + 40);
});
ink.fillStyle = '#333'; ink.font = '12px system-ui, sans-serif';
ink.fillText('Prices after a (15% off) and then b (.99 pricing), both through the shared group:', 8, 200);
}
main();
</script>