Raw Memory Indexing with Byte Address Buffers in DirectX

Byte address buffers in DirectX provide a mechanism for untyped, raw memory access, bypassing standard graphics pipeline format conversions. This article explores how these buffers operate under the hood, how High-Level Shader Language (HLSL) addresses memory using direct byte offsets, the mandatory 4-byte alignment constraints, and how developers leverage intrinsic load and store operations to pack and unpack arbitrary data structures directly on the GPU.

In standard DirectX shader programming, resources like structured buffers or typed textures enforce strict data formatting. When a shader accesses a structured buffer, the hardware calculates memory locations automatically based on the size of the defined structure and the array index. Byte address buffers—declared in HLSL as ByteAddressBuffer for read-only access or RWByteAddressBuffer for read-write access—strip away this abstraction. Instead of viewing memory as an array of structs or pixels, the shader sees the resource as a contiguous block of raw bytes.

Raw indexing operates by providing an explicit byte offset rather than an element index. When executing a read operation, the shader specifies the precise byte position from the start of the buffer. For example, instead of requesting index 2 of a custom struct, the shader manually computes the offset in bytes (such as byte 64) and reads the raw binary data present at that exact memory location.

Despite the name "byte address buffer," modern GPU hardware processes memory in 32-bit words rather than individual 8-bit bytes. Consequently, DirectX enforces a strict alignment rule: all memory addresses supplied to byte address buffer operations must be multiples of 4 bytes (DWORD alignment). Attempting to load or store from an address that is not divisible by 4 yields undefined behavior on most GPU architectures.

To read and write data, HLSL provides specialized intrinsic functions that operate on 32-bit scalar or vector quantities:

  • Load(offset): Reads a single 32-bit unsigned integer (uint) from the specified byte offset.
  • Load2(offset), Load3(offset), Load4(offset): Read two, three, or four sequential 32-bit unsigned integers starting at the offset, mapping up to 128 bits in a single call.
  • Store(offset, value) and its vector variants (Store2, Store3, Store4): Write raw 32-bit unsigned integers into the buffer at the given byte location.

Because these intrinsics natively yield or accept unsigned 32-bit integers (uint), raw indexing relies on HLSL's bit-reinterpretation functions. Developers use asfloat(), asint(), or bit-shifting operations to interpret the returned raw bits as floating-point numbers, signed integers, or packed formats (such as 16-bit half-floats or 8-bit color channels).

Byte address buffers also support atomic operations via HLSL intrinsics like InterlockedAdd, InterlockedMin, and InterlockedCompareExchange. These functions accept raw byte offsets, allowing concurrent shader threads to synchronize and manipulate specific words within the unstructured memory block safely.

By bypassing type systems and runtime stride definitions, raw memory indexing through byte address buffers offers maximum flexibility. This capability is widely utilized in advanced rendering techniques, such as GPU-driven rendering pipelines, indirect draw argument generation, variable-rate geometry processing, and custom acceleration structure traversals where data formats vary dynamically.