Course outline
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.
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
vec3fstarts 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
Add a matrix
In the first example, add
["model", "mat4x4f"]to the end of the packed order:model: mat4x4f @ 32, and the size becomes96 bytes. - 2
Label the buffer
In the second example, add
label: "small glow buffer"to the 16-byte buffer. The message becomesBinding size (16) of [Buffer "small glow buffer"]….
Code & diagrams
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 0With 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.
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]).
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-utilscompute struct offsets from the WGSL source and give named views into oneArrayBuffer, 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
The problem this solves: the glow needs its centre, size, brightness and colour in the fragment shader. WebGL had
uniform2fand 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
Alignment. Each member starts at the next multiple of its alignment:
f324 bytes (align 4),vec2f8 (align 8),vec4f16 (align 16), andvec3f12 bytes, aligned to 16. The struct's size rounds up to its largest alignment. Socentre: vec2f, radius: f32, brightness: f32, colour: vec3f, time: f32packs into 32 bytes; the same members starting withcolourtake 48, with 4 bytes of padding aftercolourand 12 at the end. - 3
The silent bug. Write the second order as 8 floats in a row, as JavaScript sees it, and the shader reads
centreat byte 16 (float 4),radiusat 24 andbrightnessat 28, which holds 0: the glow vanishes, no error. Order members so 4-byte fields fill the gap after eachvec3f, or write each member at its offset. - 4
Bind groups.
createBindGroup({ layout, entries: [{ binding: 0, resource: { buffer } }] })connects a buffer to the shader's@group(0) @binding(0). Withlayout: "auto",pipeline.getBindGroupLayout(0)returns the layout the pipeline worked out from the shader. In a pass,setBindGroup(0, group). - 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_DSThere). 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
Why does vec3f cause layout bugs, and how do you avoid them?
What can WebGPU validation catch about uniforms, and what can't it?
Practice
Lay out struct Light { position: vec3f, intensity: f32, colour: vec3f, range: f32 }.
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