WGSL Memory Layout and Address Spaces
Lay out WGSL structs so a host-side typed array writes to the exact byte offsets the shader reads, pick the correct address space, and avoid the vec3 16-byte trap.
Quick Reference
WGSL is the shading language of WebGPU 1.0-stable: Chrome 113+, Safari 26+, Firefox 141+.
The six WGSL address spaces:
| Address space | Access mode | Host-shareable | Use for |
|---|---|---|---|
function |
read_write |
No | Local var inside a function (the default there) |
private |
read_write |
No | Module-scope per-invocation var (the module-scope default) |
workgroup |
read_write |
No | var<workgroup> shared across one compute workgroup |
uniform |
read only |
Yes | var<uniform> host buffer binding, read-only in the shader |
storage |
read or read_write |
Yes | var<storage> host buffer binding |
handle |
read |
No | Textures and samplers |
AlignOf and SizeOf per type (host buffers must honour both):
| Type | AlignOf | SizeOf | Note |
|---|---|---|---|
i32, u32, f32 |
4 | 4 | |
f16 |
2 | 2 | Requires enable f16; plus the shader-f16 feature |
atomic<i32>, atomic<u32> |
4 | 4 | workgroup/storage only |
vec2<f32> |
8 | 8 | |
vec3<f32> |
16 | 12 | The trap: alignment 16 but size 12 |
vec4<f32> |
16 | 16 | |
mat2x2<f32> |
8 | 16 | C columns aligned like vecR |
mat3x3<f32> |
16 | 48 | Each column is a vec3 padded to 16 |
mat4x4<f32> |
16 | 64 | |
array<E, N> |
AlignOf(E) |
N * stride |
stride = RoundUp(AlignOf(E), SizeOf(E)) |
A struct's AlignOf is the maximum alignment of its members. Each member is
placed at the next offset that is a multiple of its own alignment. The struct's
SizeOf is rounded up to the struct's alignment.
Uniform address space extra constraint: every array element stride MUST be a
multiple of 16, and a struct or array member MUST start at a 16-byte multiple.
This is the std140-like rule. array<f32, 4> in a uniform buffer is 64 bytes,
not 16, because each f32 is padded to a 16-byte stride.
Host-side copy alignment (256-byte dynamic offsets, 256-byte bytesPerRow) is
covered by webgpu-core-memory-model. This skill does NOT duplicate it.
Decision Tree
Choosing an address space?
├── Local scratch inside one function -> function (the default; no annotation)
├── Module-scope per-invocation state -> private (the module-scope default)
├── Shared across one compute workgroup -> workgroup (compute only, zero-init)
├── Host buffer the shader only reads -> uniform (read; std140-like rules)
├── Host buffer the shader reads or writes -> storage (read or read_write)
└── A texture or a sampler -> handle (declared via @group/@binding)
Laying out a host-matched struct?
├── Target is a uniform buffer?
│ ├── Contains a bare scalar followed by a vec3/vec4/struct/array?
│ │ └── The later member jumps to the next 16-byte offset. Add an explicit
│ │ padding member or @align so the host array agrees.
│ ├── Contains array<scalar, N>?
│ │ └── Each element is padded to a 16-byte stride. Use array<vec4<f32>, N>
│ │ or wrap the scalar in a @size(16) struct.
│ └── Otherwise -> compute every offset from AlignOf, then size the host
│ Float32Array to the struct's rounded-up SizeOf.
└── Target is a storage buffer?
├── Need a variable-length tail? -> runtime-sized array<E> as the LAST
│ member only; read its length with arrayLength(&ptr).
└── No 16-byte array-stride rule applies, but base AlignOf still does.
Core Patterns
Pattern 1: ALWAYS place a vec3 at a 16-byte offset
vec3<f32> has alignment 16. After a bare f32 at offset 0, a following vec3f
lands at offset 16, leaving offsets 4-15 as padding. ALWAYS compute the host
typed-array indices from offset / 4, NEVER from naive field order.
struct Params {
scale : f32, // offset 0, size 4
// offsets 4..15 are padding (vec3 alignment forces the jump)
origin : vec3f, // offset 16, size 12
} // AlignOf 16, SizeOf 32 (rounded up from 28)
const data = new Float32Array(8); // 32 bytes / 4
data[0] = scale; // byte offset 0
data[4] = origin[0]; // byte offset 16, NOT 4
data[5] = origin[1];
data[6] = origin[2];
Pattern 2: NEVER expect array<scalar, N> to pack tightly in a uniform buffer
In the uniform address space every array element stride is rounded up to a
multiple of 16. array<f32, 4> is 64 bytes. ALWAYS use array<vec4<f32>, N> so
the stride is naturally 16, or move the array into a storage buffer.
// WRONG intent: 16 bytes. ACTUAL in uniform: 64 bytes (16-byte stride each).
struct Bad { weights : array<f32, 4>, }
// CORRECT: 4 floats packed into one vec4, total 16 bytes.
struct Good { weights : vec4f, }
Pattern 3: ALWAYS put a runtime-sized array last and only in storage
A runtime-sized array (array<E> with no count) is legal ONLY as the final
member of a struct in the storage address space. Query its element count with
arrayLength(&ptr). NEVER declare one in function, private, or uniform.
struct Particles {
count : u32,
// u32 at 0; vec4 array starts at offset 16 (16-byte alignment)
data : array<vec4f>, // runtime-sized, MUST be the last member
}
@group(0) @binding(0) var<storage, read_write> particles : Particles;
fn total() -> u32 { return arrayLength(&particles.data); }
Pattern 4: ALWAYS use @align and @size to make layout explicit
@align(n) raises a member's minimum alignment (n a power of two). @size(n)
raises a member's minimum byte size, adding trailing padding. Use them to force a
host-friendly layout instead of leaving padding implicit.
struct Camera {
@size(16) tag : f32, // 4 bytes of data, padded to 16 so view starts at 16
view : mat4x4f, // offset 16, size 64
proj : mat4x4f, // offset 80, size 64
} // SizeOf 144
Pattern 5: ALWAYS pass a pointer to mutate caller memory
A ptr<address_space, store_type, access_mode> is formed with & and
dereferenced with *. WGSL passes parameters by value, so a helper that must
write back to caller memory ALWAYS takes a pointer parameter.
fn add_one(p : ptr<function, f32>) { *p = *p + 1.0; }
fn caller() {
var x : f32 = 41.0;
add_one(&x); // x is now 42.0
}
Pattern 6: NEVER let the host Float32Array size disagree with SizeOf
The host buffer size MUST equal the WGSL struct's rounded-up SizeOf. A struct
ending in a vec3f rounds its size up to the next multiple of 16. Size the
typed array from that rounded value, NEVER from the sum of member sizes.
struct Light { dir : vec3f, intensity : f32, } // AlignOf 16, SizeOf 16
const light = new Float32Array(4); // 16 bytes: dir at 0..2, intensity at 3
Common Anti-Patterns
A host struct
{ a: f32, b: vec3f }mapped straight to a WGSLuniformstruct, writingbat byte offset 4. WHY it fails:vec3fhas alignment 16, so the shader readsbfrom offset 16. The host writes land in the padding gap and the shader reads garbage.array<f32, N>in auniformbuffer with a host array ofN * 4bytes. WHY it fails: the uniform std140-like rule pads every element to a 16-byte stride, so elementilives at offseti * 16, noti * 4. Only element 0 reads correctly.A runtime-sized
array<f32>declared in auniformbuffer or as a non-final struct member. WHY it fails: runtime-sized arrays are legal ONLY as the last member of astoragestruct. Any other placement is a shader-creation error.
Critical Warnings
- NEVER assume
vec3<f32>occupies 12 bytes for layout. Its alignment is 16; a following member starts at the next 16-byte offset. - NEVER expect tight packing for
array<scalar, N>in auniformbuffer. Every element is padded to a 16-byte stride. - NEVER declare a runtime-sized
array<E>outside thestorageaddress space or anywhere but the last struct member. - NEVER size a host
Float32Arrayfrom the sum of member sizes. Use the WGSL struct'sSizeOfrounded up to its alignment. - NEVER write to a
uniformbuffer member from the shader. Theuniformaddress space isreadonly. - NEVER mutate caller memory by reassigning a value parameter. WGSL passes by
value; pass a
ptrand dereference with*.
Reference Files
references/methods.md: the six address spaces with access modes and host-shareability,AlignOf/SizeOfrules for every type, the uniform 16-byte rule, and pointer syntax.references/examples.md: a correctly-laid-out uniform struct matched between WGSL and a JS typed array, and a storage struct with a runtime array.references/anti-patterns.md: layout mistakes with WHY-it-fails explanations.
Related Skills
webgpu-core-memory-model: host-side copy alignment (256-byte dynamic offsets, 256-bytebytesPerRow) that this skill deliberately does not cover.webgpu-wgsl-syntax: WGSL types, declarations, and thevaraddress-space syntax this skill builds on.webgpu-syntax-buffers: creating theGPUBufferobjects these layouts fill.webgpu-syntax-bind-groups: bindinguniformandstoragebuffers into aGPUBindGroupso@group/@bindingresolve.