Buffer Alignment

Buffer Alignment and minBindingSize Rules

Every byte count in the buffer API is aligned to something:

Alignment rules for buffers (default limits on this device)
Value Must be a multiple of Limit or rule
writeBuffer() offset and size, mapped sizes 4 Fixed
Uniform binding and dynamic offsets 256 minUniformBufferOffsetAlignment
Storage binding and dynamic offsets 256 minStorageBufferOffsetAlignment
copyBufferToBuffer() offsets and size 4 Fixed

minBindingSize is the smallest range a binding accepts; with the default 0 the check moves to every draw or dispatch. Four bytes bound to a vec4f uniform failed at bind group creation under an auto layout ("Binding size (4) ... is smaller than the minimum binding size (16)"), but under an explicit layout without minBindingSize only at dispatch ("requires a buffer binding which is at least 16 bytes"). Set it to the WGSL struct size.

A uniform buffer split into 256-byte slots for dynamic offsets, drawn as a memory map, with the alignment and minBindingSize errorsHTMLLive
<!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);
}
function label(text, x, y, size = 12, color = '#2b2b2b', font = 'system-ui, sans-serif', align = 'left') {
  ink.font = `${size}px ${font}`; ink.fillStyle = color; ink.textAlign = align; ink.fillText(text, x, y);
}

const code = /* wgsl */ `
struct Cover { color: vec4f, x: f32 }                   // 20 bytes of WGSL, padded to 32
@group(0) @binding(0) var<uniform> cover: Cover;        // moved by a dynamic offset
@vertex fn vs(@builtin(vertex_index) v: u32) -> @builtin(position) vec4f {
  let q = vec2f(f32(v & 1), f32(v >> 1));
  return vec4f(cover.x + q.x * 0.3, 0.2 + q.y * 0.7, 0, 1);
}
@fragment fn fs() -> @location(0) vec4f { return cover.color; }`;

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 SLOT = device.limits.minUniformBufferOffsetAlignment;   // 256 on most devices
  const COUNT = 4, STRUCT = 32;

  const data = new Float32Array(SLOT * COUNT / 4);
  const colors = [[0.08, 0.40, 0.75], [0.85, 0.40, 0.18], [0.16, 0.56, 0.30], [0.55, 0.30, 0.65]];
  colors.forEach((c, i) => { data.set([...c, 1, -0.85 + i * 0.45], i * SLOT / 4); });
  const buffer = device.createBuffer({ size: data.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
  device.queue.writeBuffer(buffer, 0, data);                       // all slots, one write

  const layout = device.createBindGroupLayout({ entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
    buffer: { type: 'uniform', hasDynamicOffset: true, minBindingSize: STRUCT } }] });
  const module = device.createShaderModule({ code });
  const pipeline = device.createRenderPipeline({ layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
    primitive: { topology: 'triangle-strip' }, vertex: { module }, fragment: { module, targets: [{ format }] } });
  const group = device.createBindGroup({ layout, entries: [{ binding: 0, resource: { buffer, size: STRUCT } }] });

  // Two mistakes, each in an error scope: a 128-byte dynamic offset, and a binding smaller than minBindingSize.
  device.pushErrorScope('validation');
  const bad = device.createCommandEncoder();
  const bp = bad.beginRenderPass({ colorAttachments: [{ view: device.createTexture({ size: [4, 4], format, usage: GPUTextureUsage.RENDER_ATTACHMENT }).createView(), loadOp: 'clear', storeOp: 'store' }] });
  bp.setPipeline(pipeline); bp.setBindGroup(0, group, [128]); bp.draw(4); bp.end(); bad.finish();
  const offsetError = await device.popErrorScope();
  device.pushErrorScope('validation');
  device.createBindGroup({ layout, entries: [{ binding: 0, resource: { buffer, size: 4 } }] });
  const sizeError = await device.popErrorScope();

  const encoder = device.createCommandEncoder();
  const pass = encoder.beginRenderPass({ colorAttachments: [{ view: context.getCurrentTexture().createView(),
    clearValue: [0.97, 0.96, 0.93, 1], loadOp: 'clear', storeOp: 'store' }] });
  pass.setPipeline(pipeline);
  const offsets = new Uint32Array(COUNT).map((_, i) => i * SLOT);
  for (let i = 0; i < COUNT; i++) {
    pass.setBindGroup(0, group, offsets, i, 1);      // offsets[i], no new array per draw
    pass.draw(4);
  }
  pass.end();
  device.queue.submit([encoder.finish()]);

  // The memory map: each slot is SLOT bytes; only the first 32 hold the struct.
  const mapW = 560 / COUNT;
  for (let i = 0; i < COUNT; i++) {
    const x = 20 + i * mapW;
    ink.fillStyle = '#e7e3dc'; ink.fillRect(x, 170, mapW - 4, 34);
    ink.fillStyle = `rgb(${colors[i].map((c) => c * 255).join(',')})`; ink.fillRect(x, 170, (mapW - 4) * STRUCT / SLOT, 34);
    label(`offset ${i * SLOT}`, x, 222, 11, '#555', 'ui-monospace, monospace');
  }
  label(`One ${SLOT * COUNT}-byte buffer, ${COUNT} slots of minUniformBufferOffsetAlignment = ${SLOT} bytes (coloured: the 32-byte struct)`, 20, 160, 11.5, '#222');
  label('setBindGroup(0, group, [128]):', 20, 256, 12, '#222');
  label(offsetError ? offsetError.message.split('\n')[0].slice(0, 86) : 'accepted', 20, 274, 11, '#8a2b2b', 'ui-monospace, monospace');
  label('A 4-byte binding where minBindingSize is 32:', 20, 304, 12, '#222');
  label(sizeError ? sizeError.message.split('\n')[0].slice(0, 86) : 'accepted', 20, 322, 11, '#8a2b2b', 'ui-monospace, monospace');
  label('Set minBindingSize to the WGSL struct size, so the check happens once, at createBindGroup().', 20, 348, 11.5, '#444');
}
main();
</script>