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):
- The
float3occupiesregister0.xyz(bytes 0–11). - The
float2requires 8 bytes, but only 4 bytes remain inregister0(register0.w). - The compiler leaves
register0.wempty as padding and places thefloat2atregister1.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
floatorint), 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_majorkeyword. - In a column-major matrix, each column is placed into a separate
16-byte register. A
float4x4occupies 4 registers (64 bytes). - A
float3x3matrix 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:
- Sort by Size: Declare variables in descending order
of size (
float4/ matrices first, followed byfloat3,float2, and scalars). - Explicit Padding: Always declare explicit dummy
variables in both C++ and HLSL to fill unused components (e.g., pairing
a
float3with afloatto complete a 16-byte block). - Array Packing: Avoid arrays of scalars; use
float4arrays instead and unpack the components manually in shader logic. - 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.