GLSL std140 Layout Padding Rules for vec3

Understanding the memory layout of uniform buffer objects is critical for preventing subtle data alignment bugs between host code and shaders. In OpenGL Shading Language (GLSL), the std140 layout standard enforces explicit sizing and alignment rules for all uniform block members. This article details the specific padding and alignment behavior applied to the three-component floating-point vector (vec3) under the std140 specification.

The Base Alignment Rule for vec3

According to the OpenGL specification for std140, members of a uniform block are aligned according to their base alignment:

  • A scalar (e.g., float, int, uint) has a base alignment of 4 bytes.
  • A two-component vector (vec2) has a base alignment of 8 bytes.
  • A three-component vector (vec3) has a base alignment of 16 bytes.
  • A four-component vector (vec4) has a base alignment of 16 bytes.

Even though a vec3 only occupies 12 bytes of active data (three 32-bit floats), its base alignment is rounded up to 16 bytes—identical to the base alignment of a vec4.

Offset and Boundary Placement

Because the base alignment of a vec3 is 16 bytes, any vec3 declared inside a std140 uniform block must begin at an offset that is an exact multiple of 16 bytes.

If preceding data does not cleanly end on a 16-byte boundary, the compiler automatically inserts padding bytes immediately before the vec3 to push its starting offset to the next 16-byte multiple.

Example: Alignment Padding Before vec3

layout (std140) uniform ExampleBlock {
    float a;    // Offset: 0, Size: 4 bytes
                // Padding: 12 bytes (offsets 4 to 15)
    vec3 b;     // Offset: 16, Size: 12 bytes (offsets 16 to 27)
};

In this example, 12 bytes of padding are placed after float a so that vec3 b can align to byte offset 16.

Trailing Padding and Packing

While a vec3 aligns to 16 bytes, its raw size remains 12 bytes. If another variable with a smaller alignment requirement follows the vec3, it can occupy the remaining 4 bytes of that 16-byte chunk, provided its alignment constraints are satisfied.

layout (std140) uniform PackedExample {
    vec3 a;     // Offset: 0, Size: 12 bytes (offsets 0 to 11)
    float b;    // Offset: 12, Size: 4 bytes (offsets 12 to 15)
    vec4 c;     // Offset: 16, Size: 16 bytes (offsets 16 to 31)
};

Because float b requires only 4-byte alignment, it fits directly into offset 12 without additional padding. However, if another vec3 or vec4 follows vec3 a, it cannot use offset 12 because both types require 16-byte alignment. In that case, 4 bytes of implicit padding are added after vec3 a.

Arrays of vec3

Under std140, array elements are always aligned to multiples of 16 bytes. Consequently, an array of vec3 does not tightly pack elements into sequential 12-byte blocks. Instead, each element has a stride of 16 bytes:

layout (std140) uniform ArrayBlock {
    vec3 vectors[2];
};
  • vectors[0]: Offsets 0 to 11 (Offsets 12 to 15 are padding)
  • vectors[1]: Offsets 16 to 27 (Offsets 28 to 31 are padding)

Each vec3 in an array wastes 4 bytes as trailing padding.

Matching the Layout in C++

When mirroring a GLSL uniform block in host code such as C++, you must explicitly account for these padding rules to maintain alignment:

struct ExampleBlock {
    alignas(16) float a;
    alignas(16) float b[3]; // vec3 represented with 16-byte alignment
};

struct ArrayBlock {
    struct Vec3Padded {
        float x, y, z;
        float padding; // Explicitly fills the 4-byte gap to reach 16 bytes
    };
    Vec3Padded vectors[2];
};

To avoid complex manual padding and potential data mismatches, using vec4 instead of vec3 in std140 blocks is widely considered a best practice in graphics programming.