How SV_GroupIndex Linearizes 3D Coordinates in DirectX

In DirectX High Level Shading Language (HLSL), SV_GroupIndex is a system-generated value that flattens a thread's three-dimensional coordinate within a thread group into a single, one-dimensional scalar index. This article explains the mathematical formula DirectX uses to linearize these 3D coordinates, how the layout is determined by the numthreads attribute, and how to utilize this index for efficient resource access such as groupshared memory.

The Thread Group Context

When writing a compute shader in HLSL, the dimensions of a thread group are defined using the [numthreads(X, Y, Z)] attribute. Within this group, each individual thread has a unique 3D position represented by the SV_GroupThreadID semantic, with components ranging as follows:

  • \(0 \le x < X\)
  • \(0 \le y < Y\)
  • \(0 \le z < Z\)

While working with 3D coordinates is intuitive for volumetric calculations, image processing, or spatial partitioning, hardware architectures and data buffers operate linearly. DirectX provides SV_GroupIndex to bridge this gap, mapping the multi-dimensional coordinates directly to an integer ranging from \(0\) to \((X \times Y \times Z) - 1\).

The Linearization Formula

DirectX linearizes 3D thread coordinates using row-major ordering (lexicographical order), meaning that the \(X\) dimension increments fastest, followed by \(Y\), and finally \(Z\).

The formula used by the runtime is:

\[\text{SV\_GroupIndex} = z \times (X \times Y) + y \times X + x\]

Where:

  • \(x, y, z\) are the components of the current thread's SV_GroupThreadID.
  • \(X, Y\) are the group dimensions defined in [numthreads(X, Y, Z)].
  • The \(Z\) dimension size determines the upper bound but is not directly used to compute intermediate strides.

Practical Example

Consider a compute shader configured with the following attribute:

[numthreads(4, 4, 2)]

Here, the dimensions are \(X = 4\), \(Y = 4\), and \(Z = 2\), yielding a total of \(32\) threads per group.

If a thread has an SV_GroupThreadID of (2, 3, 1):

  1. Calculate the layer offset for the Z dimension: \(1 \times (4 \times 4) = 16\)
  2. Calculate the row offset for the Y dimension: \(3 \times 4 = 12\)
  3. Add the column offset for the X dimension: \(2\)
  4. Sum the offsets: \(16 + 12 + 2 = 30\)

The thread with SV_GroupThreadID(2, 3, 1) receives an SV_GroupIndex of 30.

Primary Applications of SV_GroupIndex

The linearized index is primarily used for:

  1. Groupshared Memory Indexing: groupshared memory is allocated locally per thread group. Linear arrays in groupshared memory are declared with a single size (e.g., groupshared float cache[32]), and SV_GroupIndex provides a 1:1 mapping for threads to read from or write to these arrays without manual index calculation.
  2. Cooperative Reductions: Algorithms like parallel reduction or prefix sums require power-of-two thread stepping, which is far simpler to compute using a 1D sequence than a 3D coordinate space.
  3. Execution Masking: When a single thread within a group needs to initialize data or execute a scalar operation, checking if (SV_GroupIndex == 0) ensures only one thread runs the block regardless of group geometry.