Command Palette

Search for a command to run...

Hectal
Course outline
Topic 14.4Advanced ~21 min · step 4 of 6

Buffers, Bind Groups and Pipelines

In one line

In WebGPU, the values a shader reads (uniforms) live in buffers you fill yourself, at offsets WGSL's alignment rules decide. A bind group connects buffers to the shader's bindings. A wrong layout makes the shader read wrong bytes silently; a wrong size is stopped by validation.

0/6 · 0%

Think of it like this

A lunch box with moulded compartments. Each item has a fixed slot, some slots are bigger than their content, and if you put the fruit in the sandwich slot nobody complains until lunch. A WGSL struct is the moulded box.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

alignment
The byte multiple a value must start at: a vec3f starts at 0, 16, 32…
padding
Unused bytes so the next member starts at its alignment, or so the size is a multiple of the largest alignment.
uniform buffer
A GPU buffer whose bytes a shader reads as a struct of uniforms.
bind group
A set of resources (buffers, textures, samplers) attached to a shader's numbered bindings.
usage
Flags fixed when a buffer is created that say how it may be used (UNIFORM, COPY_DST, VERTEX, MAP_READ).

Step by step

01Work out the offsets

The first example applies the rules in plain JavaScript to both orders. In the naive order centre starts at 16, after 4 bytes of padding.

02See what the shader reads

Its last line: the naive order written tightly makes the shader read centre as 75, 60, radius 1 and brightness 0. A glow with brightness 0 looks like "nothing drawn".

03Draw with a uniform buffer

The second example fills the 32-byte buffer in struct order, makes a bind group from getBindGroupLayout(0), and draws one triangle that covers the screen; the fragment shader computes the glow. With WebGPU you see a red glow in the middle.

04Catch the size error

Then a 16-byte buffer inside pushErrorScope("validation"): popErrorScope() resolves with the error, naming the sizes, the binding and the call.

Try it yourself

  1. 1

    Add a matrix

    In the first example, add ["model", "mat4x4f"] to the end of the packed order: model: mat4x4f @ 32, and the size becomes 96 bytes.

  2. 2

    Label the buffer

    In the second example, add label: "small glow buffer" to the 16-byte buffer. The message becomes Binding size (16) of [Buffer "small glow buffer"]….

Code & diagrams

WGSL struct layout, worked out in JavaScript New tab

No GPU needed: the WGSL rules for uniform structs.

Console

(nothing was printed)

Expected console output

packed order: 32 bytes for 32 bytes of data
    centre: vec2f @ 0
    radius: f32 @ 8
    brightness: f32 @ 12
    colour: vec3f @ 16
    time: f32 @ 28
naive order : 48 bytes for 32 bytes of data
    colour: vec3f @ 0
    centre: vec2f @ 16 (after 4 bytes of padding)
    radius: f32 @ 24
    brightness: f32 @ 28
    time: f32 @ 32
shader reads centre 75, 60 | radius 1 | brightness 0
A uniform buffer, a bind group, and a size error New tab

With WebGPU: a red glow, `glow drawn: 32 bytes of uniforms`, then `too-small buffer: Binding size (16) of [Buffer (unlabeled)] is smaller than the minimum binding size (32).` and three lines saying where. Without WebGPU it says so.

idle

Console

Run the scene to see its console.

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Reorder the struct, not the JavaScript

In the second example's WGSL, move colour: vec3f to the top of the struct; use new Float32Array(12) and write the floats in the new order with no gaps: data.set([1.0, 0.1, 0.05, canvas.width / 2, canvas.height / 2, canvas.height * 0.4, 1.0, 0]).

terminal
$ Run it in a WebGPU browser
── what you'll see ──
glow drawn: 48 bytes of uniforms
No glow, only the dark background, and no validation error: the buffer is big enough. Measured: the middle pixel stayed at the background colour.

Myth vs fact

Myth

A struct's size is the sum of its members' sizes.

Fact

Only when they line up. The same five members take 32 or 48 bytes depending on order.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Libraries such as webgpu-utils compute struct offsets from the WGSL source and give named views into one ArrayBuffer, removing this class of bug.

  • ▸

    Uniform bindings are limited (maxUniformBufferBindingSize, at least 64 KiB). Larger data goes in storage buffers (var<storage>), which have looser layout rules (array strides need not round up to 16) and can be written by compute shaders (Topic 13.5).

Remember this

  1. 1

    The problem this solves: the glow needs its centre, size, brightness and colour in the fragment shader. WebGL had uniform2f and friends; WebGPU wants bytes in a buffer, read at offsets WGSL decides. If JavaScript and the shader disagree, the shader reads garbage, without any error.

  2. 2

    Alignment. Each member starts at the next multiple of its alignment: f32 4 bytes (align 4), vec2f 8 (align 8), vec4f 16 (align 16), and vec3f 12 bytes, aligned to 16. The struct's size rounds up to its largest alignment. So centre: vec2f, radius: f32, brightness: f32, colour: vec3f, time: f32 packs into 32 bytes; the same members starting with colour take 48, with 4 bytes of padding after colour and 12 at the end.

  3. 3

    The silent bug. Write the second order as 8 floats in a row, as JavaScript sees it, and the shader reads centre at byte 16 (float 4), radius at 24 and brightness at 28, which holds 0: the glow vanishes, no error. Order members so 4-byte fields fill the gap after each vec3f, or write each member at its offset.

  4. 4

    Bind groups. createBindGroup({ layout, entries: [{ binding: 0, resource: { buffer } }] }) connects a buffer to the shader's @group(0) @binding(0). With layout: "auto", pipeline.getBindGroupLayout(0) returns the layout the pipeline worked out from the shader. In a pass, setBindGroup(0, group).

  5. 5

    What validation catches. A 16-byte buffer for the 32-byte struct is refused: Binding size (16) of [Buffer (unlabeled)] is smaller than the minimum binding size (32). It checks sizes, formats and usage flags (fixed at creation: UNIFORM | COPY_DST here). It can't check that your bytes mean what the shader thinks. queue.writeBuffer(buffer, 0, data) copies a typed array in before the next submitted commands run.

Explain it without notes

01

Why does vec3f cause layout bugs, and how do you avoid them?

02

What can WebGPU validation catch about uniforms, and what can't it?

Practice

01

Lay out struct Light { position: vec3f, intensity: f32, colour: vec3f, range: f32 }.

02

Fill the packed glow struct for a glow at (100, 50), radius 30, brightness 0.5, colour (1, 0.1, 0.05), time 2.

Done when you can

  • You can compute WGSL struct offsets by hand

  • You write uniform data at the right offsets

  • You can create a bind group and set it in a pass

  • You catch validation errors with error scopes and labels