Manual Buffer Indexing with SV_VertexID in DirectX

In DirectX pipelines (Direct3D 11 and Direct3D 12), developers can decouple geometry processing from the fixed-function Input Assembler by utilizing programmable vertex pulling. Instead of declaring rigid input layouts, vertex attributes can be stored in Shader Resource Views (SRVs) and fetched manually within the vertex shader using the system-generated value SV_VertexID. This approach provides flexible data formats, minimizes driver overhead from layout transitions, and streamlines GPU data sharing between compute and graphics stages.

Understanding SV_VertexID

The SV_VertexID semantic is an input-assembler-generated 32-bit unsigned integer (uint) that assigns an index to each vertex invocation. For non-indexed draw calls (such as Draw or DrawInstanced), the hardware assigns an identifier based on the vertex position in the draw sequence, taking the base vertex offset into account. For indexed draw calls (such as DrawIndexedInstanced), the Input Assembler reads the traditional index buffer and passes the resulting index value into the vertex shader through SV_VertexID.

Configuring the Pipeline for Manual Pulling

To pull vertex data manually, the application bypasses standard Input Assembler attribute extraction:

  1. Omit the Input Layout: Set the input layout to nullptr (or omit input layout elements in Direct3D 12 pipeline state objects).
  2. Bind Data as SRVs: Instead of binding vertex buffers via IASetVertexBuffers or traditional vertex buffer views, bind the buffers as Shader Resource Views (SRVs) accessible to the vertex shader stage.
  3. Execute Empty Draw Calls: Issue standard draw calls (e.g., DrawInstanced(vertexCount, instanceCount, 0, 0)). Because the vertex count is non-zero, the rasterizer and scheduler dispatch the requested number of shader threads, routing the sequential counter to SV_VertexID.

HLSL Implementation Using StructuredBuffer

The simplest way to manually fetch vertex attributes is with a StructuredBuffer<T>. When declared in HLSL, the shader compiler exposes array-style indexing, allowing SV_VertexID to directly subscript the buffer.

struct Vertex
{
    float3 position;
    float2 uv;
};

StructuredBuffer<Vertex> g_VertexBuffer : register(t0);
cbuffer SceneConstants : register(b0)
{
    float4x4 g_WorldViewProj;
};

struct VSOutput
{
    float4 position : SV_Position;
    float2 uv : TEXCOORD0;
};

VSOutput VSMain(uint vertexID : SV_VertexID)
{
    VSOutput output;
    
    // Manually index into the buffer using SV_VertexID
    Vertex v = g_VertexBuffer[vertexID];
    
    output.position = mul(float4(v.position, 1.0f), g_WorldViewProj);
    output.uv = v.uv;
    
    return output;
}

HLSL Implementation Using ByteAddressBuffer

For custom data packing, non-uniform strides, or zero-copy interop with compute pipelines, ByteAddressBuffer (raw buffers) provides low-level memory addressing. The vertex shader multiplies SV_VertexID by the stride of the vertex structure in bytes to locate the exact memory offset.

ByteAddressBuffer g_RawVertexBuffer : register(t0);

VSOutput VSMain(uint vertexID : SV_VertexID)
{
    VSOutput output;

    // Define byte stride: 12 bytes (float3) + 8 bytes (float2) = 20 bytes
    uint stride = 20;
    uint byteOffset = vertexID * stride;

    // Load data at calculated byte offsets
    float3 pos = asfloat(g_RawVertexBuffer.Load3(byteOffset));
    float2 uv = asfloat(g_RawVertexBuffer.Load2(byteOffset + 12));

    output.position = mul(float4(pos, 1.0f), g_WorldViewProj);
    output.uv = uv;

    return output;
}

Architectural Benefits

Using SV_VertexID for manual buffer indexing offers distinct performance and architectural advantages:

  • State Cache Efficiency: Eliminates pipeline state object (PSO) variations caused by disparate vertex input layouts, keeping pipeline switches lightweight.
  • Dynamic Attribute Fetching: Allows shaders to conditionally branch and avoid fetching unused vertex attributes (e.g., normal or tangent vectors during shadow map passes).
  • Direct Compute Integration: Meshes generated or animated via Compute Shaders can be consumed directly by the vertex shader without needing conversions or multiple buffer views.