Skip to content

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 std430 layout 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:

  1. 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))]

  2. 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:

GPU Memory Buffer Layout (32 Bytes total)
CACHE LINE 0 (16 Bytes) Offsets 0 - 15
a u32 (Bytes 0-3)
12-BYTE PADDING HOLE
CACHE LINE 1 (16 Bytes) Offsets 16 - 31
b.x f32 (16-19)
b.y f32 (20-23)
b.z f32 (24-27)
c f32 (28-31)
Member a (u32, 4B)
Member b (vec3<f32>, 12B)
Member c (f32, 4B)
Padding Hole (12B)

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.