Interlocked Operations on Raw Buffers in DirectX

In DirectX compute kernels, thousands of threads execute concurrently and often need to write to the same memory addresses within raw buffers (RWByteAddressBuffer). Without synchronization, simultaneous access causes data races, torn writes, and unpredictable output. Interlocked operations solve this by performing atomic read-modify-write transactions directly at the hardware memory controller or cache level, ensuring that memory updates occur sequentially and without thread collisions. This article explains how interlocked operations operate on raw buffers, how the GPU enforces atomicity, and the best practices for maintaining both thread safety and high performance.

The Race Condition Problem in Raw Buffers

A raw buffer in High-Level Shader Language (HLSL) is represented as a ByteAddressBuffer or RWByteAddressBuffer. These buffers lack structured data layouts; instead, they treat memory as a contiguous block of bytes accessed via 32-bit aligned byte offsets.

When multiple compute shader threads attempt to update the same 32-bit address using standard read and write instructions:

  1. Thread A reads the current value from the buffer.
  2. Thread B reads the same value before Thread A finishes writing its update.
  3. Both threads compute their updates independently.
  4. Both threads write their values back, causing one thread's update to overwrite and discard the other.

This standard read-modify-write cycle is non-atomic. Because GPU wavefronts (or warps) execute in lockstep across SIMD lanes, resource contention on identical byte addresses occurs frequently.

How Hardware Guarantees Atomicity

Interlocked operations eliminate race conditions by converting the multi-step read-modify-write cycle into a single, indivisible hardware transaction.

When an HLSL intrinsic such as InterlockedAdd is invoked on an RWByteAddressBuffer:

  • Memory Controller Arbitration: The request is routed to the GPU’s L2 cache or memory controllers, which house dedicated atomic arithmetic logic units (ALUs).
  • Serialization at the Cache Line: If multiple threads target the exact same 32-bit offset, the memory controller serializes the operations at the hardware level.
  • Indivisible Execution: The read, computation, and write-back occur in an uninterruptible cycle for each request. No intervening memory access from any other thread can read intermediate data or write a competing value.

Because this synchronization occurs at the memory architecture level, it requires no software locks, operating system semaphores, or thread context switches.

Common HLSL Interlocked Functions on Raw Buffers

HLSL provides a family of interlocked intrinsics tailored for RWByteAddressBuffer resources. These operations require byte offsets that are strictly multiples of 4 (32-bit aligned):

  • InterlockedAdd(dest, value, original_value): Atomically adds a value to the target 32-bit integer.
  • InterlockedMin(dest, value, original_value) and InterlockedMax(...): Compares and updates the target with the minimum or maximum value.
  • InterlockedOr(...), InterlockedAnd(...), InterlockedXor(...): Executes bitwise manipulation atomically.
  • InterlockedExchange(dest, value, original_value): Sets a new value and retrieves the previous value atomically.
  • InterlockedCompareExchange(dest, compare_value, value, original_value): Replaces the target value only if it matches compare_value. This forms the foundation for lock-free algorithms on the GPU.

In each intrinsic, retrieving the original_value is optional, but doing so allows threads to obtain a unique index, such as an allocation slot in a shared global append counter.

Addressing Performance Bottlenecks and Contention

While interlocked operations guarantee correctness, excessive contention can severely degrade compute performance. When thousands of threads attempt atomic operations on the same memory address simultaneously, execution collapses from massive parallelism into sequential execution at the memory controller.

To maintain thread safety while maximizing throughput:

  1. Use Wave Intrinsics First: Before writing to global raw buffers, use wave-level operations (such as WaveActiveSum) to aggregate values across all active lanes in a wave. Only a single lane per wave then needs to execute the InterlockedAdd to the raw buffer, cutting global atomic operations by a factor of 32 or 64.
  2. Utilize groupshared Memory: For algorithms requiring workgroup-wide coordination, use interlocked operations on fast, on-chip groupshared memory first. Once the threadgroup resolves its local updates, a single thread updates the global RWByteAddressBuffer.
  3. Minimize Contention Hotspots: Stagger memory offsets or use hashing strategies to distribute atomic operations across multiple cache lines whenever possible.