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
vec3is aligned to 16 bytes (the size of avec4), 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 avec4(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, oruint; 8 bytes fordoubleor 64-bit integers). - A
vec3requires 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
- 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. - Seamless Data Transfers: Buffer memory can be
populated using direct memory copies (
memcpy) or mapped pointers straight from CPU collections (such asstd::vector<Particle>) without pre-processing or serialization passes. - 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.