HLSL Constant Buffer Alignment Rules in DirectX

In DirectX High-Level Shader Language (HLSL), constant buffers (cbuffer) group uniform data to be sent from the CPU to the GPU. To ensure efficient access, the hardware requires constant buffer data to adhere to strict 16-byte memory alignment and packing rules. Mismatches between C++ host struct layouts and HLSL constant buffer layouts lead to silent data corruption, making an exact understanding of HLSL packing behavior essential for rendering development.

The 16-Byte (128-Bit) Register Rule

HLSL organizes constant buffers into an array of 4-component 32-bit registers (equivalent to a float4 or 16 bytes: x, y, z, and w).

  • Every register is exactly 16 bytes wide.
  • Variables are packed into the current register sequentially as long as they fit without crossing the 16-byte boundary.
  • A single variable cannot straddle two 16-byte registers. If a variable does not completely fit into the remaining space of the current register, it is pushed entirely to the beginning of the next 16-byte register.

For example, if a buffer defines a float3 (12 bytes) followed by a float2 (8 bytes):

  1. The float3 occupies register0.xyz (bytes 0–11).
  2. The float2 requires 8 bytes, but only 4 bytes remain in register0 (register0.w).
  3. The compiler leaves register0.w empty as padding and places the float2 at register1.xy (bytes 16–23).

Array Alignment Rules

Arrays in HLSL constant buffers do not pack tightly by default:

  • Every element in an array is aligned to a 16-byte boundary.
  • Even if an array holds scalar values (like float or int), each element consumes a full 16-byte register, leaving the remaining 12 bytes unused.
  • For instance, float values[4] consumes 64 bytes (four separate 16-byte registers), not 16 bytes.

Matrix Alignment Rules

Matrices are treated as collections of vectors:

  • By default, HLSL treats matrices as column-major unless configured otherwise via compiler flags or the row_major keyword.
  • In a column-major matrix, each column is placed into a separate 16-byte register. A float4x4 occupies 4 registers (64 bytes).
  • A float3x3 matrix occupies three 16-byte registers; each 3-component vector takes 12 bytes of a register, leaving the 4th component (.w) as unused padding.

Structure and Nested Type Rules

When defining custom structs within a cbuffer:

  • The start of a struct is always aligned to a 16-byte boundary.
  • Nested structs do not pack into trailing space from preceding variables.
  • The total size of a struct is rounded up to a multiple of 16 bytes before any subsequent variable begins.

Controlling Alignment with packoffset

To eliminate ambiguity and prevent unexpected compiler packing, HLSL provides the packoffset keyword. This forces a variable to occupy an exact register and sub-component location:

cbuffer SceneData : register(b0)
{
    float4x4 viewProj : packoffset(c0);
    float3 lightDir   : packoffset(c4.x);
    float lightIntensity : packoffset(c4.w); // Explicitly fills the unused space after float3
};

CPU-to-GPU Synchronization Best Practices

To avoid alignment errors between C++ and HLSL:

  1. Sort by Size: Declare variables in descending order of size (float4 / matrices first, followed by float3, float2, and scalars).
  2. Explicit Padding: Always declare explicit dummy variables in both C++ and HLSL to fill unused components (e.g., pairing a float3 with a float to complete a 16-byte block).
  3. Array Packing: Avoid arrays of scalars; use float4 arrays instead and unpack the components manually in shader logic.
  4. Buffer Size Constraint: The total size of any constant buffer allocated in DirectX 11 and DirectX 12 must be a multiple of 256 bytes when bound to the pipeline.