GLSL Memory Alignment: int16_t vs float Vectors

In GLSL interface blocks, memory alignment dictates how data maps between CPU memory and GPU storage buffers or uniform buffers. While standard 32-bit float vectors adhere to standard 4-, 8-, and 16-byte alignment rules, 16-bit integer (int16_t) vectors operate on a base component size of 2 bytes, leading to smaller 2-, 4-, and 8-byte alignments under compact memory rules. Understanding these differences across std140, std430, and scalar memory layouts is essential to prevent unintended padding, buffer mismatch bugs, and inefficient memory bandwidth usage.

Base Component Sizes and Alignment Rules

In standard GLSL packing rules, the base alignment of a vector is defined by the size of its underlying scalar type multiplied by either 2 (for 2-component vectors) or 4 (for 3- and 4-component vectors). Because a standard float is 4 bytes and an int16_t (from extensions such as GL_EXT_shader_explicit_arithmetic_types_int16) is 2 bytes, the fundamental alignment requirements scale down proportionally.

Type Size (Bytes) Base Alignment (Bytes)
float 4 4
vec2 8 8
vec3 12 16
vec4 16 16
int16_t 2 2
i16vec2 4 4
i16vec3 6 8
i16vec4 8 8

Both 3-component vectors (vec3 and i16vec3) round their alignment up to the size of a 4-component vector of that type. Consequently, vec3 requires 16-byte alignment despite using only 12 bytes of data, leaving a 4-byte padding hole. Similarly, i16vec3 requires 8-byte alignment despite using only 6 bytes, leaving a 2-byte padding gap before the next aligned member.

Differences in std140 Layout

The std140 standard, commonly mandated for Uniform Buffer Objects (UBOs), enforces aggressive alignment rules that can eliminate the memory-saving benefits of 16-bit vectors:

  • Array Strides: Under std140, every element in an array is rounded up to the alignment of a 16-byte vector (vec4), regardless of the element type. An array of float has an element stride of 16 bytes. An array of int16_t or i16vec2 also gets padded to a 16-byte stride per element.
  • Structure Alignment: Any structure or nested structure within std140 has its base alignment rounded up to a multiple of 16 bytes.
  • Individual Vectors: Non-arrayed i16vec2 and i16vec4 members can still pack tightly at 4-byte and 8-byte offsets respectively, whereas vec2 requires 8 bytes and vec4 requires 16 bytes.

Differences in std430 Layout

The std430 layout rule, available in Shader Storage Buffer Objects (SSBOs), eliminates the mandatory 16-byte array padding, allowing 16-bit types to achieve significant bandwidth and footprint reductions:

  • Array Strides: Under std430, array stride equals the base alignment of the array element itself. An array of vec2 has an 8-byte stride, and an array of vec4 has a 16-byte stride. In contrast, an array of i16vec2 has a 4-byte stride, and an array of i16vec4 has an 8-byte stride.
  • Three-Component Arrays: An array of vec3 has a 16-byte stride (aligned to vec4), while an array of i16vec3 has an 8-byte stride (aligned to i16vec4).

Impact of Scalar Block Layout

When using extensions like GL_EXT_scalar_block_layout (standard in modern Vulkan pipelines), alignment constraints match the underlying scalar type rather than vector dimensions:

  • float vectors: Alignment drops to 4 bytes for all vector types (vec2, vec3, vec4). A vec3 takes exactly 12 bytes with 4-byte alignment, leaving zero internal padding.
  • int16_t vectors: Alignment drops to 2 bytes for all vector types (i16vec2, i16vec3, i16vec4). An i16vec3 takes exactly 6 bytes with 2-byte alignment.

Host-Side Struct Synchronization

To ensure CPU-GPU data parity when uploading data:

  • In C++ using standard rules, a vec4 maps to a 16-byte aligned struct (alignas(16)), while an i16vec4 maps to an 8-byte aligned struct (alignas(8)).
  • A 3-component vec3 requires 4 bytes of explicit tail padding on the CPU to prevent data misalignment in std140 and std430. An i16vec3 requires 2 bytes of tail padding to satisfy its 8-byte alignment footprint.