Auto vs Explicit Layout

Auto Layout Versus Explicit Layout Trade-offs

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:

One explicit layout let both pipelines share a group. Keep 'auto' for single-pipeline jobs and demos.

The three traps of layout: 'auto' hit in error scopes, then one explicit layout shared by two compute pipelinesHTMLLive
<!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>