Scalar Block Layout for C++ Structs and GLSL SSBOs

Matching CPU data structures written in C++ with GPU storage buffers in GLSL has traditionally required tedious manual padding due to rigid alignment standards like std140 and std430. The scalar block layout extension solves this problem by aligning member variables based solely on their component scalar sizes rather than vector-based rules. This article explains how scalar block layout eliminates alignment mismatches, enables seamless 1:1 memory mappings, and simplifies data transfer between the CPU and GPU.

The Traditional Alignment Problem

Standard GLSL buffer layouts—primarily std140 and std430—enforce strict, hardware-centric alignment rules that rarely match the default layout of C++ structures.

Under std140 and std430:

  • A vec3 is aligned to 16 bytes (the size of a vec4), even though it only consumes 12 bytes of data.
  • In std140, arrays of any type round the stride of each element up to the base alignment of a vec4 (16 bytes).
  • Nested structures often enforce a base alignment rounded up to a multiple of 16 bytes.

In contrast, standard C++ compilers follow natural data alignment rules. A struct containing three float values (such as a standard 3D vector library type) occupies 12 bytes without trailing padding unless explicitly aligned using alignas(16) or manual padding fields. This discrepancy often forces developers to write duplicate mirror structures on the CPU, manually insert padding variables, or use error-prone bit-packing tricks.

How Scalar Block Layout Works

The scalar block layout layout modifier (enabled via GL_EXT_scalar_block_layout in GLSL and supported via VK_EXT_scalar_block_layout in Vulkan) replaces vector-based alignment rules with scalar alignment rules.

Under a scalar layout:

  • Every type is aligned to the size of its fundamental scalar component (e.g., 4 bytes for float, int, or uint; 8 bytes for double or 64-bit integers).
  • A vec3 requires only 4-byte alignment, meaning it takes exactly 12 contiguous bytes with no mandatory 4-byte padding at the end.
  • Arrays and matrices are packed tightly according to their component scalar size rather than rounding elements up to 16 bytes.

Direct C++ to GLSL Mapping

By adopting scalar layout, a C++ structure can directly mirror a GLSL shader storage buffer object (SSBO) without modification.

Consider a mixed-member struct:

// C++ Struct
struct Particle {
    float position[3]; // 12 bytes
    float lifetime;    // 4 bytes
    int id;            // 4 bytes
};

In standard C++, position occupies 12 bytes, and lifetime immediately follows at offset 12 because 12 is naturally aligned to a 4-byte float boundary. The entire struct occupies 20 bytes.

Under std430, GLSL would treat a vec3 as 16-byte aligned, shifting lifetime to offset 16 and inflating the struct size to 24 or 32 bytes depending on layout rules.

With scalar block layout applied in GLSL:

#extension GL_EXT_scalar_block_layout : require

struct Particle {
    vec3 position; // 12 bytes at offset 0
    float lifetime;// 4 bytes at offset 12
    int id;        // 4 bytes at offset 16
};

layout(scalar, binding = 0) buffer ParticleBuffer {
    Particle particles[];
};

The GLSL compiler positions lifetime at offset 12 and id at offset 16. The memory layout matches the C++ struct identically.

Key Benefits

  1. Elimination of Manual Padding: Developers no longer need to insert artificial dummy variables (float _pad;) into C++ structs or maintain dual versions of data models.
  2. Seamless Data Transfers: Buffer memory can be populated using direct memory copies (memcpy) or mapped pointers straight from CPU collections (such as std::vector<Particle>) without pre-processing or serialization passes.
  3. Memory Bandwidth Optimization: Removing artificial padding reduces overall buffer sizes, saving GPU memory and reducing the required memory bandwidth for cache lines during high-throughput compute workloads.