device.createBuffer({ size, usage, mappedAtCreation, label }) allocates zero-filled GPU memory. usage ORs together GPUBufferUsage flags for every way the buffer will be used; anything undeclared fails validation:
| Flags (value) | Allows | Rule |
|---|---|---|
| MAP_READ (1), MAP_WRITE (2) | mapAsync() from JavaScript | Combine only with COPY_DST or COPY_SRC |
| COPY_SRC (4), COPY_DST (8) | Copies, writeBuffer() | writeBuffer() needs COPY_DST |
| INDEX (16), VERTEX (32) | setIndexBuffer(), setVertexBuffer() | Render Pipelines |
| UNIFORM (64), STORAGE (128) | Bind group buffer bindings | Bind Group Layouts |
| INDIRECT (256), QUERY_RESOLVE (512) | Indirect draws, query results | Indirect Draws and Bundles, Profiling and Beyond |
On this machine MAP_READ | STORAGE failed with "If a buffer usage contains BufferUsage::MapRead the only other allowed usage is BufferUsage::CopyDst", usage 0 failed, and a 1 TiB buffer failed against the default maxBufferSize of 268435456 (256 MiB). Mappable buffers are staging memory, never shader resources: shaders work on a separate STORAGE buffer that you copy in or out of.
<!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="380"></canvas>
<canvas id="labels" width="600" height="380"></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 flags = ['MAP_READ', 'MAP_WRITE', 'COPY_SRC', 'COPY_DST', 'INDEX', 'VERTEX', 'UNIFORM', 'STORAGE', 'INDIRECT', 'QUERY_RESOLVE'];
const U = GPUBufferUsage ?? {};
const tries = [
['storage buffer you can write', 64, () => U.STORAGE | U.COPY_DST],
['staging buffer for read-back', 64, () => U.MAP_READ | U.COPY_DST],
['mappable and a shader resource', 64, () => U.MAP_READ | U.STORAGE],
['no usage at all', 4, () => 0],
['mesh data', 64, () => U.VERTEX | U.INDEX | U.COPY_DST],
['indirect args written by compute', 16, () => U.INDIRECT | U.STORAGE],
['1 TiB of storage', 2 ** 40, () => U.STORAGE]];
// One cell per (row, flag): lit when that bit is set in the row's usage.
const code = /* wgsl */ `
@group(0) @binding(0) var<storage> usage: array<u32>;
struct Out { @builtin(position) pos: vec4f, @location(0) @interpolate(flat) cell: u32 }
@vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) i: u32) -> Out {
let q = vec2f(f32(v & 1), f32(v >> 1));
let px = vec2f(190 + f32(i % 10) * 28, 76 + f32(i / 10) * 36) + q * vec2f(24, 27);
return Out(vec4f(px.x / 300 - 1, 1 - px.y / 190, 0, 1), i);
}
@fragment fn fs(in: Out) -> @location(0) vec4f {
let on = (usage[in.cell / 10] >> (in.cell % 10)) & 1;
return select(vec4f(0.89, 0.88, 0.85, 1), vec4f(0.08, 0.40, 0.75, 1), on == 1);
}`;
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 results = [];
for (const [name, size, usage] of tries) {
device.pushErrorScope('validation');
device.createBuffer({ size, usage: usage() }); // never throws: problems become GPU errors
results.push([name, size, usage(), await device.popErrorScope()]);
}
const usages = device.createBuffer({ size: tries.length * 4, usage: U.STORAGE | U.COPY_DST });
device.queue.writeBuffer(usages, 0, new Uint32Array(results.map((r) => r[2])));
const module = device.createShaderModule({ code });
const pipeline = device.createRenderPipeline({ layout: 'auto', primitive: { topology: 'triangle-strip' },
vertex: { module }, fragment: { module, targets: [{ format }] } });
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);
pass.setBindGroup(0, device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: usages } }] }));
pass.draw(4, tries.length * 10);
pass.end();
device.queue.submit([encoder.finish()]);
ink.save(); ink.font = '10.5px ui-monospace, monospace'; ink.fillStyle = '#444';
flags.forEach((f, k) => { ink.save(); ink.translate(206 + k * 28, 70); ink.rotate(-Math.PI / 3); ink.fillText(`${f} ${1 << k}`, 0, 0); ink.restore(); });
ink.restore();
results.forEach(([name, size, usage, error], i) => {
const y = 94 + i * 36;
ink.textAlign = 'right'; ink.font = '12px system-ui, sans-serif'; ink.fillStyle = '#222';
ink.fillText(name, 182, y);
ink.font = '10.5px ui-monospace, monospace'; ink.fillStyle = '#777';
ink.fillText(`usage ${usage}, ${size >= 2 ** 30 ? '1 TiB' : size + ' B'}`, 182, y + 12);
ink.textAlign = 'left'; ink.font = 'bold 12px system-ui, sans-serif';
ink.fillStyle = error ? '#b03a28' : '#2a8f4d';
ink.fillText(error ? 'fails' : 'ok', 478, y + 4);
});
const mapErr = results[2][3], big = results[6][3];
ink.font = '11px system-ui, sans-serif'; ink.fillStyle = '#8a2b2b';
if (mapErr) ink.fillText(mapErr.message.split('\n')[0].slice(0, 96), 8, 370);
if (big) { ink.fillStyle = '#555'; ink.fillText(`maxBufferSize on this device: ${device.limits.maxBufferSize.toLocaleString('en-US')} bytes`, 8, 350); }
}
main();
</script>