| name | webgpu-impl-instancing-indirect |
| description | Use when drawing many instances, issuing indirect draws or dispatches, or building GPU-driven rendering. Prevents corrupt geometry from wrong indirect buffer strides and the silently ignored firstInstance. Covers instanced draws, drawIndirect and drawIndexedIndirect with exact buffer layouts, dispatchWorkgroupsIndirect, GPU-driven rendering, and multiDrawIndirect. Keywords: instancing, instanced draw, instanceCount, drawIndirect, drawIndexedIndirect, dispatchWorkgroupsIndirect, indirect buffer, GPU-driven rendering, multiDrawIndirect, firstInstance, instance_index, how do I draw many objects.
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires WebGPU 1.0-stable. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
WebGPU Instancing and Indirect Draws
Draw one mesh thousands of times in a single call, and let the GPU itself decide how
many vertices, instances, or workgroups to process by reading draw parameters from a
buffer instead of from JavaScript.
Quick Reference
WebGPU 1.0-stable (Chrome 113+, Safari 26+, Firefox 141+).
| Indirect call | Buffer size | u32 values, in exact order |
|---|
drawIndirect | 16 bytes | vertexCount, instanceCount, firstVertex, firstInstance |
drawIndexedIndirect | 20 bytes | indexCount, instanceCount, firstIndex, baseVertex, firstInstance |
dispatchWorkgroupsIndirect | 12 bytes | workgroupCountX, workgroupCountY, workgroupCountZ |
All values are tightly packed little-endian u32. The GPU reads EXACTLY this many
bytes starting at indirectOffset.
| Method | Signature |
|---|
| Non-indexed instanced | draw(vertexCount, instanceCount, firstVertex, firstInstance) |
| Indexed instanced | drawIndexed(indexCount, instanceCount, firstIndex, baseVertex, firstInstance) |
| Non-indexed indirect | drawIndirect(indirectBuffer, indirectOffset) |
| Indexed indirect | drawIndexedIndirect(indirectBuffer, indirectOffset) |
| Indirect dispatch | dispatchWorkgroupsIndirect(indirectBuffer, indirectOffset) |
Rules that ALWAYS hold:
- Indirect buffer usage MUST include
GPUBufferUsage.INDIRECT. The common combination
is COPY_DST | INDIRECT. For a GPU-written buffer, add STORAGE.
indirectOffset MUST be a multiple of 4.
- A non-zero
firstInstance in an INDIRECT draw requires the indirect-first-instance
feature. Without it the value is forced to 0 (silent no-op, not an error).
firstInstance in a DIRECT draw / drawIndexed call works without any feature.
Decision Tree
Drawing many copies of one mesh?
├─ Counts and per-instance data known on the CPU
│ └─ Direct instanced draw: draw / drawIndexed with instanceCount.
│ Per-instance data → vertex buffer stepMode "instance"
│ OR storage buffer indexed by @builtin(instance_index).
│
├─ Counts decided on the GPU (culling, particle spawn, LOD)
│ └─ Indirect draw: a compute pass writes the indirect buffer,
│ the render pass reads it with drawIndirect / drawIndexedIndirect.
│ No CPU readback of the count.
│
├─ Compute dispatch size decided on the GPU
│ └─ dispatchWorkgroupsIndirect, buffer written by a prior pass.
│
└─ Hundreds of distinct meshes, each with its own indirect record
└─ One indirect record per mesh in an array buffer; loop
drawIndexedIndirect with a stepping indirectOffset.
Chrome 131+ only: multiDrawIndexedIndirect collapses the loop
into one call. Feature-gate it, never assume availability.
Instancing answers "how do I draw the same thing many times". Indirect answers "how do
I draw without the CPU knowing the count". GPU-driven rendering combines both.
Core Patterns
Pattern 1: ALWAYS pass instanceCount, never loop draw calls
NEVER call draw() once per object in a loop when the objects share a mesh. ALWAYS
issue one instanced draw.
pass.setPipeline(pipeline);
pass.setVertexBuffer(0, cubeVertices);
pass.draw(36, 5000, 0, 0);
Pattern 2: ALWAYS choose one per-instance data path
Per-instance attributes (model matrix, color) live EITHER in a vertex buffer with
stepMode: "instance" OR in a storage buffer read by @builtin(instance_index).
const layout = {
arrayStride: 16,
stepMode: "instance",
attributes: [{ shaderLocation: 2, offset: 0, format: "float32x4" }],
};
// Storage-buffer path: index the buffer by the instance.
@group(0) @binding(0) var<storage, read> instances: array<mat4x4f>;
@vertex fn vs(@builtin(instance_index) i: u32, @location(0) pos: vec3f)
-> @builtin(position) vec4f {
return instances[i] * vec4f(pos, 1.0);
}
instance_index ALWAYS includes the draw's firstInstance base offset.
Pattern 3: ALWAYS write the full indirect record
NEVER leave indirect buffer bytes uninitialized. The GPU reads every byte of the
record. Write all 4 (or 5, or 3) u32 values, including zeros.
const params = new Uint32Array([36, 5000, 0, 0]);
const indirectBuffer = device.createBuffer({
size: 16,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.INDIRECT,
});
device.queue.writeBuffer(indirectBuffer, 0, params);
pass.drawIndirect(indirectBuffer, 0);
Pattern 4: ALWAYS match the stride to the draw kind
A non-indexed drawIndirect record is 16 bytes. An indexed drawIndexedIndirect
record is 20 bytes. NEVER reuse one stride for the other call.
const indexed = new Uint32Array([indexCount, 5000, 0, 0, 0]);
const buf = device.createBuffer({ size: 20,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.INDIRECT });
device.queue.writeBuffer(buf, 0, indexed);
pass.setIndexBuffer(indexBuffer, "uint32");
pass.drawIndexedIndirect(buf, 0);
Pattern 5: ALWAYS feature-gate firstInstance in indirect draws
A non-zero firstInstance inside an indirect record requires the
indirect-first-instance feature. ALWAYS request it conditionally and keep
firstInstance at 0 when it is absent.
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice({
requiredFeatures: adapter.features.has("indirect-first-instance")
? ["indirect-first-instance"] : [],
});
Pattern 6: ALWAYS let the GPU write the indirect buffer for GPU-driven rendering
A compute pass culls and writes the indirect buffer. The render pass in the SAME
queue.submit reads it. NEVER read the count back to the CPU.
const indirectBuffer = device.createBuffer({
size: 20,
usage: GPUBufferUsage.INDIRECT | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
const encoder = device.createCommandEncoder();
const cull = encoder.beginComputePass();
cull.setPipeline(cullPipeline);
cull.setBindGroup(0, cullBindGroup);
cull.dispatchWorkgroups(Math.ceil(objectCount / 64));
cull.end();
const draw = encoder.beginRenderPass(renderPassDesc);
draw.setPipeline(drawPipeline);
draw.setIndexBuffer(indexBuffer, "uint32");
draw.drawIndexedIndirect(indirectBuffer, 0);
draw.end();
device.queue.submit([encoder.finish()]);
Common Anti-Patterns
-
Using a 20-byte stride for a non-indexed drawIndirect (or 16 for indexed).
WHY it fails: the GPU reads exactly 16 bytes for drawIndirect and exactly 20 for
drawIndexedIndirect. A wrong stride misaligns the next record, so firstVertex /
baseVertex / firstInstance are read from garbage, producing corrupt geometry or
a validation error.
-
Leaving indirect buffer bytes uninitialized. WHY it fails: writing only
vertexCount and instanceCount leaves firstVertex and firstInstance as
undefined memory; the GPU still reads them, causing out-of-range vertex fetches.
-
A non-zero firstInstance in an indirect draw without the
indirect-first-instance feature. WHY it fails: the value is silently forced to
0. The draw renders, no error fires, and instances appear at the wrong base offset
with no diagnostic.
Critical Warnings
- NEVER call
draw() in a per-object CPU loop for objects that share a mesh. Use
instanceCount.
- NEVER pass an
indirectOffset that is not a multiple of 4. It fails validation and
invalidates the pass encoder.
- NEVER reuse a 16-byte stride for
drawIndexedIndirect or a 20-byte stride for
drawIndirect.
- NEVER omit any
u32 field from an indirect record. Write zeros explicitly.
- NEVER assume
multiDrawIndirect / multiDrawIndexedIndirect exist. They are
experimental, Chrome 131+, behind the chromium-experimental-multi-draw-indirect
feature. Always feature-detect and provide the loop-of-drawIndirect path.
- NEVER read the GPU-written indirect count back to the CPU in the render frame. The
command encoder already orders the compute pass before the render pass.
- NEVER set
stepMode: "vertex" on a vertex buffer that holds per-instance data. The
attribute would advance per vertex and every instance would look identical.
Reference Files
references/methods.md : full signatures for draw, drawIndexed, drawIndirect,
drawIndexedIndirect, dispatchWorkgroupsIndirect, exact buffer layouts, the
indirect-first-instance feature, and experimental multiDrawIndirect.
references/examples.md : verified working code for instanced draw, indirect draw
with a populated buffer, and a GPU-driven cull plus indirect render.
references/anti-patterns.md : mistakes with WHY-it-fails analysis.
Related skills: webgpu-syntax-render-pipeline (vertex buffer layouts, stepMode),
webgpu-syntax-compute-pipeline (compute passes that write indirect buffers),
webgpu-wgsl-vertex-shaders (@builtin(instance_index), per-instance attributes),
webgpu-impl-performance (when GPU-driven rendering pays off, render bundles),
webgpu-impl-compute-usecases (particle systems feeding indirect draws).