← All columns
WEBGPU2026.06.152 min read

Organising WGSL bind groups

A bind-group layout that survives a growing pipeline count — including how I got it wrong first.

WebGPU exposes shader resources through bind groups. I put everything in @group(0) without thinking about it, and it fell apart somewhere past the fifth pipeline.

What went wrong

With everything in one group, any single differing resource forces an entirely new bind group.

The camera matrix (shared by every pipeline), material data (per pipeline) and model matrix (per draw) all lived in the same group — so drawing one object meant rebuilding a bind group that included the camera matrix all over again.

Split by update frequency

The fix is simple: give each update frequency its own group.

Group Contents Updated
@group(0) camera, time, viewport once per frame
@group(1) material, textures on pipeline switch
@group(2) model matrix, instance data per draw
@group(0) @binding(0) var<uniform> frame: FrameUniforms;
@group(1) @binding(0) var<uniform> material: MaterialUniforms;
@group(1) @binding(1) var albedo: texture_2d<f32>;
@group(2) @binding(0) var<uniform> model: ModelUniforms;

Now setBindGroup(0, ...) is called once at the top of the frame. Only group 2 gets rebuilt inside the draw loop.

The spec guarantees at least four bind groups, so a design that fits in three runs everywhere.

Share the layouts

GPUBindGroupLayout objects are reusable across pipelines. Every pipeline should receive the same group 0 layout.

const frameLayout = device.createBindGroupLayout({ entries: [...] });

const pipeline = device.createRenderPipeline({
  layout: device.createPipelineLayout({
    bindGroupLayouts: [frameLayout, materialLayout, modelLayout],
  }),
  // ...
});

layout: 'auto' is convenient for the first pipeline, but it generates a distinct layout object per pipeline, which makes bind groups unshareable. Switching to explicit layouts as soon as you write the second pipeline saves time overall.

Dynamic offsets, or not

Per-draw model matrices can also live in one large uniform buffer, switched by dynamic offset.

pass.setBindGroup(2, modelBindGroup, [i * alignedSize]);

That collapses bind group creation to a single object, which helps — but offsets must be multiples of minUniformBufferOffsetAlignment, typically 256 bytes. Spending 256 bytes on a 64-byte matrix is a real cost, so while draw counts are low, separate bind groups read better.