Alignment
When passing structures between your host program (JavaScript/TypeScript) and shaders, you must follow strict Structure Layout & Memory Alignment rules.
Unlike general-purpose CPU programming where compilers automatically hide alignment and padding details, GPU memory architectures enforce explicit layouts. Understanding these rules is essential to prevent mismatched CPU-to-GPU structures, which lead to corrupted data or silent rendering failures.
Why Is Memory Alignment Enforced?
At the physical hardware level, GPU execution units process data by fetching memory in parallel coalesced blocks (typically 32-byte, 64-byte, or 128-byte cache lines).
- Hardware Coalescing: If a 32-bit float spans across a cache-line boundary, the GPU would require two separate memory clock cycles to retrieve a single value, severely reducing memory bandwidth.
- CPU-to-GPU Serialization: WebGPU pipelines require that data buffers bound as uniforms or storage buffers match the native GPU layout exactly. WebGPU adopts the Vulkan
std430layout specification to enforce these hardware boundaries.
Memory Alignment & Sizing Rules
Every WGSL data type has a compile-time alignment requirement (the starting byte offset of the variable must be a multiple of this value) and an inherent size (the actual bytes it occupies in memory).
| Type | Alignment (bytes) | Size (bytes) |
|---|---|---|
f32, i32, u32 |
\(4\) | \(4\) |
vec2<f32> |
\(8\) | \(8\) |
vec3<f32> |
\(16\) | \(12\) |
vec4<f32> |
\(16\) | \(16\) |
mat4x4<f32> |
\(16\) | \(64\) (4 columns of 16 bytes each) |
The vec3 Trap
A vec3<f32> contains 3 floats, meaning it only occupies \(12\) bytes of raw data. However, its alignment requirement is \(16\) bytes. If you place a variable immediately after a vec3, the compiler forces it to start at the next \(16\)-byte boundary, creating an automatic \(4\)-byte padding hole in your structure!
Mathematical Formulation of Struct Layouts
The final layout of a custom structure is calculated recursively based on its members:
-
Structure Alignment: The alignment requirement of a structure \(\text{align}(S)\) is the maximum alignment of any of its members: [\text{align}(S) = \max_{m \in S} (\text{align}(m))]
-
Structure Size: The final size of a structure \(\text{size}(S)\) is its unpadded size rounded up to the nearest multiple of its structure alignment \(\text{align}(S)\): [\text{size}(S) = \lceil \text{unpadded_size}(S) / \text{align}(S) \rceil \times \text{align}(S)]
Interactive 32-Byte Structure Memory Layout
Consider the WGSL structure MyData shown below:
struct MyData {
a : u32, // Offset 0, size 4 (aligned to 4)
b : vec3<f32>, // Offset 16, size 12 (aligned to 16) - 12 bytes of padding after 'a'
c : f32, // Offset 28, size 4 (aligned to 4)
} // Total size: 32 bytes (aligned to 16, the largest member's alignment)
Here is a visual, byte-by-byte representation of MyData laid out in memory across two \(16\)-byte (128-bit) GPU cache boundaries:
Enforcing Layouts Dynamically
To maintain absolute control over memory serialization or match a specific host data structure exactly, WGSL supports manual layout attributes:
@align(N): Forces a member's alignment requirement to be a multiple of N bytes.@size(N): Forces a member's overall size to occupy exactly N bytes, appending trailing padding to that specific member if necessary.
The interactive shader panel demonstrates how offsets and sizes are computed dynamically using these custom attributes.
/*
* Copyright ©2026 Michael R. Bernstein. Licensed under Apache 2.0.
* See root README.md for global project-wide upstream attributions.
*/
struct Offsets {
u_align: u32,
v_align: u32,
w_align: u32,
size_of_struct: u32,
}
// Define a struct to demonstrate alignment, size and padding rules.
struct CustomVehicle {
// Offset 0, size 4 (aligned to 4)
@align(4) num_wheels: u32,
// Offset 16, size 12 (aligned to 16 due to vec3's inherent alignment requirement)
// This leaves 12 bytes of padding between num_wheels and mass_kg!
@align(16) mass_kg: vec3<f32>,
// Offset 28, size 4, but explicitly padded to size 8 using the @size attribute
@size(8) cargo_id: u32,
}
// Since WGSL does not have runtime sizeof/alignof operators,
// we define a constant struct that represents the calculated layout offsets
// to feed the Tour of WGSL's value visualizer.
const computed_offsets = Offsets(
4u, // u_align: alignment of num_wheels
16u, // v_align: alignment of mass_kg (vec3<f32>)
8u, // w_align: size of cargo_id (with @size(8))
32u // size_of_struct: total size of CustomVehicle struct
);