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.