Every byte count in the buffer API is aligned to something:
| 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.
<!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>