GLSL Struct Uniform Layouts: Default vs UBO

In OpenGL Shading Language (GLSL), structure uniforms behave fundamentally differently depending on whether they are declared as loose, default uniforms or placed within a Uniform Buffer Object (UBO). Default uniform layouts are managed entirely by the driver with opaque packing, requiring individual location queries for each struct member. In contrast, structures within UBOs follow rigid, standardized memory layout specifications—most notably std140—which enforce strict alignment and padding rules so that memory can be directly mirrored from host CPU memory to GPU buffers.

Default Memory Layout for Structure Uniforms

When a struct is declared outside of an interface block, it belongs to the default uniform block:

struct Material {
    vec3 ambient;
    float shininess;
};
uniform Material u_material;

In this configuration, the OpenGL driver flattens the structure into discrete, independent uniform variables. The driver has full discretion over where and how these values are stored in GPU memory. Consequently:

  • No Fixed Memory Offset: There is no standardized byte offset or memory padding defined by the specification. The host application cannot write to the structure as a contiguous block of raw binary data.
  • Component-Wise Location Queries: Each field inside the struct receives its own independent uniform location. The CPU-side application must retrieve locations using individual member strings (such as glGetUniformLocation(program, "u_material.ambient") and glGetUniformLocation(program, "u_material.shininess")).
  • Sequential API Calls: Values must be uploaded individually using family functions like glUniform3fv and glUniform1f.

Uniform Buffer Object (UBO) Layouts

When a structure is placed inside a uniform block, it is backed by a GPU buffer object (GL_UNIFORM_BUFFER). This allows large sets of data to be shared across shader stages and uploaded via a single memory transfer (glBufferSubData or memory mapping).

To ensure portability across different hardware vendors, GLSL provides explicit layout qualifiers, the most common being std140.

struct Material {
    vec3 ambient;    // offset 0
    float shininess; // offset 12
};

layout(std140) uniform MaterialBlock {
    Material u_material;
};

Under the std140 layout rules:

  • Base Alignment: Every type has a base alignment. A scalar float or int has a 4-byte alignment, a vec2 requires 8-byte alignment, and both vec3 and vec4 require 16-byte alignment.
  • Structure Alignment: A structure’s base alignment is equal to the alignment of its largest member, rounded up to the nearest multiple of 16 bytes (the size of a vec4).
  • Structure Padding: The total size of a structure is always rounded up to a multiple of 16 bytes. In the Material struct example above, even though ambient (12 bytes) and shininess (4 bytes) total 16 bytes, any following member or subsequent struct in an array will begin on the next 16-byte boundary.
  • Arrays of Structures: In an array of structs, each element is padded so that its start index matches the 16-byte alignment rule.

Key Architectural Differences

  • Memory Determinism: Default uniforms offer no layout predictability, making direct memory copying impossible. UBO layouts (std140 or compute-oriented std430) guarantee deterministic byte offsets across all conforming hardware.
  • Upload Efficiency: Default structs require multiple API state changes and individual uniform setters. UBO structures are uploaded as a contiguous memory block via standard buffer update mechanisms.
  • Data Density: UBO structures frequently introduce explicit padding bytes due to 16-byte alignment rules, consuming more memory per struct than the driver’s potentially optimized internal representation for default uniforms.