Structured Buffer Alignment in DirectX 11 and 12
In DirectX 11 and DirectX 12, structured buffers require strict byte stride alignment to ensure peak memory performance, deterministic address calculation, and hardware compatibility across diverse GPU architectures. A structured buffer contains an array of identically sized user-defined structures, where the byte stride specifies the exact size in bytes of each individual element. This article explores how modern GPU memory controllers, SIMD execution units, hardware cache-line constraints, and the Direct3D runtime mandate these strict alignment rules to prevent execution penalties and hardware-level faults.
GPU Memory Architecture and Memory Coalescing
Modern GPUs execute shaders in parallel groups of threads, known as warps on NVIDIA hardware or wavefronts on AMD hardware. When these threads simultaneously read or write data in a structured buffer, the GPU’s memory controller attempts to coalesce these individual memory requests into as few memory transactions as possible.
GPU memory controllers typically operate on fixed-size cache lines, commonly 32, 64, or 128 bytes in width. If a structured buffer's byte stride is properly aligned—at a minimum to a multiple of 4 bytes (standard 32-bit scalar alignment) and ideally aligned to 16 bytes (128-bit vector alignment)—adjacent threads access contiguous, predictably spaced addresses. When byte strides are unaligned or arbitrarily sized:
- Memory Divergence: Individual structure members end up straddling cache-line boundaries. A single element read might require two separate cache-line fetches instead of one.
- Bandwidth Saturation: A warp of 32 threads reading misaligned data can require double or quadruple the number of memory transactions, cutting effective memory bandwidth drastically.
Hardware Address Calculation (ALU Offsets)
Shader instructions that access structured buffers, such as HLSL's
StructuredBuffer<T> or
RWStructuredBuffer<T>, map directly to dedicated load
and store instructions in hardware ISA (Instruction Set Architecture),
such as ld_structured and
store_structured.
To locate a member within a structured buffer, the GPU hardware performs the following calculation:
\[\text{Address} = \text{BufferBase} + (\text{Index} \times \text{Stride}) + \text{MemberOffset}\]
To keep hardware address generation units (AGUs) simple and fast:
- Shift Optimization: GPUs optimize multiplication by powers of two into simple bit-shift operations. When strides conform to power-of-two alignments (or multiples of 4 bytes), address math requires minimal ALU cycles.
- Atomic and Vector Operations: Many hardware
platforms lack unaligned vector load instructions. If an HLSL structure
contains a
float4and the stride is not a multiple of 16 bytes, subsequent elements will place thatfloat4at addresses that are not 16-byte aligned. On architectures that enforce strict alignment for vector or atomic operations, this causes severe performance throttling or hardware fault exceptions.
API Requirements: DirectX 11 vs. DirectX 12
Both DirectX 11 and 12 enforce strict structural validation at the runtime and driver layers:
- DirectX 11 Specifications: When defining a buffer
using
D3D11_BUFFER_DESC, theStructureByteStrideparameter must be a multiple of 4 bytes and cannot exceed 2048 bytes. IfStructureByteStrideis not a multiple of 4, device creation or buffer view creation fails immediately via the runtime's validation layer. - DirectX 12 Explicit Alignment: In DirectX 12,
manual resource management requires adhering to additional hardware
alignment barriers. While
D3D12_BUFFER_SRV::StructureByteStridestill mandates a 4-byte multiple, the underlying resource allocations viaD3D12_RESOURCE_DESCoften require alignment to 64 KB (or 4 MB for MSAA), and descriptor offsets must align with hardware-specific limits.
HLSL Struct Packing Rules
HLSL applies strict packing rules to user-defined structures. By
default, scalar types like float or uint
occupy 4 bytes, while vector types like float3 and
float4 occupy 12 and 16 bytes, respectively.
HLSL prevents members from spanning across 16-byte boundaries (registers) in constant buffers, and it applies similar padding logic to structured buffers to preserve natural alignment:
struct BadAlignment
{
float3 position; // 12 bytes
float2 uv; // 8 bytes -> Straddles boundary without padding
};
// Total size: 20 bytes (multiple of 4, but creates misaligned vector access on element 1)
struct GoodAlignment
{
float3 position; // 12 bytes
float padding; // 4 bytes explicit padding
float2 uv; // 8 bytes
float2 padding2; // 8 bytes padding
};
// Total size: 32 bytes (aligned to 16 bytes, optimal for caching and SIMD execution)
Adhering to strict byte stride alignment is not merely an API constraint, but a fundamental requirement to align high-level shader code with the physical realities of modern parallel memory subsystems. Failure to align strides results in API validation failures, disabled hardware-level coalescing, and severe execution bottlenecks.