Structs, Constant Buffers & Packing Rules
Structs are used for stage I/O (with semantics), cbuffer contents, structured-buffer elements, and ray payloads.
13.1 Structs
Structs are used for stage I/O (with semantics), cbuffer contents, structured-buffer elements, and ray payloads. HLSL 2021 adds member functions and operator overloading.
struct Material
{
float3 albedo;
float roughness;
float3 emissive;
float metallic;
};
struct Light
{
float3 position; float range;
float3 color; float intensity;
};
Material MakeDefaultMaterial()
{
Material m = (Material)0; // zero-initialize every member
m.albedo = 0.8;
m.roughness = 0.5;
return m;
}13.2 Constant buffers
Constant buffers (CBVs) hold per-frame/per-object/per-material uniform data.
Global variables declared outside any cbuffer (non-static) are placed in an implicit $Globals cbuffer — avoid them in D3D12 code; be explicit.
// Classic syntax (SM 4+)
cbuffer PerFrame : register(b0)
{
float4x4 gView;
float4x4 gProj;
float3 gCameraPos;
float gTime;
};
// Template syntax (SM 5.1+): members accessed through the variable
struct ObjectData { float4x4 world; float4x4 worldInvTranspose; uint materialIndex; };
ConstantBuffer<ObjectData> gObject : register(b1);
float4 Use() { return float4(gCameraPos, gTime) + gObject.world[0]; }13.3 Packing rules (16-byte registers)
Constant buffers are laid out in 16-byte registers (four 32-bit components):
Variables are packed sequentially into the current register if they fit.
A variable cannot straddle a 16-byte boundary; if it doesn't fit, it starts on the next register.
Structs and arrays always start on a new register.
Each array element occupies its own register (only the last element may be partially filled) — float a[4] takes 4 registers (64 bytes), not 16 bytes.
Matrices: each column (column_major) or row (row_major) occupies one register.
The total cbuffer size is rounded up to 16 bytes; D3D12 CBV views must be 256-byte aligned.
register 0,.xyz fits in register 0.w register 1.xy doesn't fit in reg 1 (only 8 bytes left) → register 2 each element at 48, 64, 80 (16-byte stride) Matching C++ struct:
Tips: order members largest-first and pack float3 with a trailing float; use float4 arrays and index arr[i / 4][i % 4] instead of float arr[N]; verify with reflection or the -Fc disassembly, which prints every offset. Explicit offsets are possible with packoffset:
struct alignas(16) CBData
{
DirectX::XMFLOAT3 a; float b; // 0
DirectX::XMFLOAT2 c; float _pad0[2]; // 16
DirectX::XMFLOAT3 d; float _pad1; // 32
DirectX::XMFLOAT4 e[3]; // 48: use only .x of each element
DirectX::XMFLOAT4X4 m; // 96
};
static_assert(sizeof(CBData) == 160, "cbuffer layout mismatch");cbuffer Tweaks : register(b2)
{
float3 tint : packoffset(c0);
float exposure : packoffset(c0.w);
float2 jitter : packoffset(c1);
};13.4 Structured buffer and 16-bit packing differ
StructuredBuffer<T> uses tight C-like packing (4-byte alignment for 32-bit types, no 16-byte register rule; float3 = 12 bytes). This makes them easier to match with C++ structs. With -enable-16bit-types, 16-bit types pack at 2-byte granularity in both cbuffers and structured buffers. DXC's -no-legacy-cbuf-layout (Vulkan: -fvk-use-dx-layout/-fvk-use-scalar-layout) changes layouts — learn the defaults first.