Vertex Pull Rendering vs DirectX Input Assembler

Vertex pull rendering, often referred to as programmable vertex pulling, is a modern rendering technique where the GPU's programmable shader stages manually fetch vertex data from memory buffers, entirely bypassing the fixed-function Input Assembler (IA) stage. This article explores how vertex pulling operates, details the architectural limitations of conventional DirectX input layouts, and explains how manual data fetching streamlines pipeline state management, improves memory efficiency, and integrates with modern bindless rendering architectures.

The Conventional Input Assembler in DirectX

In traditional DirectX pipelines (DirectX 11 and classic DirectX 12), the Input Assembler is the first stage of the graphics pipeline. It relies on explicit vertex buffers, index buffers, and input layouts defined via D3D12_INPUT_LAYOUT_DESC or D3D11_INPUT_ELEMENT_DESC.

Developers define the memory layout of each vertex attribute (such as position, normal, UV coordinates, and tangent) using predefined formats like DXGI_FORMAT_R32G32B32_FLOAT. The hardware's fixed-function logic then reads these declarations, traverses the bound vertex buffers according to their defined strides and offsets, unpacks the data into standard registers, and passes the populated attributes directly into the vertex shader’s input parameters.

Limitations of the Traditional Approach

While the Input Assembler simplifies pipeline setup for basic use cases, it introduces several bottlenecks in high-performance rendering:

  • Pipeline State Object (PSO) Bloat: In DirectX 12, the input layout is baked directly into the PSO. If two meshes share the same shader logic but use slightly different vertex arrangements (for example, 16-bit floats instead of 32-bit floats for UVs), separate PSOs must be compiled and maintained.
  • Rigid Data Formats: The fixed-function hardware supports only standard DXGI_FORMAT types. Custom data compression schemes, variable-bit-width packing, or procedural attributes cannot be decoded at the IA stage.
  • Overfetching in Specialized Passes: Meshes often share a single interleaved vertex buffer containing positions, normals, and texture coordinates. During a depth-only prepass or shadow map generation, the hardware still has to stream or skip across non-positional data, wasting memory bandwidth unless separate position-only streams are maintained.
  • Impediment to Bindless Rendering: Modern rendering engines leverage bindless descriptor heaps to access thousands of resources through integer handles. The Input Assembler requires explicit binding of vertex buffer views to specific slots, breaking the uniformity of fully bindless designs.

What is Vertex Pull Rendering?

Vertex pull rendering bypasses the Input Assembler by setting the input layout in the PSO to nullptr (or empty) and passing only primitive topology information (such as a triangle list) to the draw call. The draw call invokes the vertex shader purely with system-generated values, primarily SV_VertexID and SV_InstanceID.

Inside the vertex shader, vertex attributes are fetched programmatically from memory resources—typically ByteAddressBuffer (raw buffer) or StructuredBuffer—using the vertex ID as an index:

ByteAddressBuffer VertexPositions : register(t0);
ByteAddressBuffer VertexAttributes : register(t1);

struct VSOutput
{
    float4 Position : SV_Position;
    float2 UV : TEXCOORD0;
};

VSOutput MainVS(uint vertexID : SV_VertexID)
{
    VSOutput output;
    
    // Explicitly load a 12-byte float3 position
    uint address = vertexID * 12;
    float3 pos = asfloat(VertexPositions.Load3(address));
    
    // Transform position
    output.Position = mul(float4(pos, 1.0f), WorldViewProjection);
    
    // Fetch and unpack UV coordinates on demand
    uint uvAddress = vertexID * 4; // 16-bit half precision packed into 32 bits
    uint packedUV = VertexAttributes.Load(uvAddress);
    output.UV = UnpackHalf2x16(packedUV);
    
    return output;
}

How Vertex Pulling Replaces the Input Assembler

Vertex pulling shifts the responsibility of address calculation, memory fetching, and format conversion from dedicated silicon to the general-purpose compute units of the GPU.

1. Decoupling Geometry from Pipeline States

Because the vertex shader consumes raw buffers rather than an explicit layout declaration, the PSO does not need to know the structure of the vertex data. A single PSO can draw any mesh, regardless of how its attributes are laid out in memory, significantly reducing PSO compilation overhead and runtime state switches.

2. Fine-Grained Attribute Stream Separation

Vertex pulling makes it simple to separate geometry into distinct attribute streams (Structure of Arrays). Vertex positions can be stored in one continuous buffer, while normals, tangents, and material coordinates reside in others. For depth passes or shadow rendering, the shader loads only from the position buffer, maximizing cache line utilization and memory bandwidth without requiring artists or asset pipelines to generate duplicate meshes.

3. Custom Compression and Bit-Packing

Without the constraint of DXGI_FORMAT definitions, developers can implement custom encoding formats. Normals can be compressed into octahedral encodings (oct32 or oct16), positions can be quantized relative to mesh bounding boxes using integer math, and weights can be stored in bit-packed fields. Shaders execute the unpacking logic via fast ALU operations, which are often cheaper than the memory bandwidth required to fetch uncompressed data.

4. Seamless Integration with Bindless Architectures

In DirectX 12 using Shader Model 6.6 dynamic resources, buffers do not need to be bound to root parameters individually. The shader can receive an integer index pointing to the buffer in the global ResourceDescriptorHeap. This allows a draw call to dynamically select its geometry buffers at runtime, paving the way for GPU-driven rendering techniques where compute shaders build draw arguments and perform geometry culling entirely on the GPU.

Hardware Realities and Considerations

On modern GPU architectures (such as AMD GCN/RDNA and NVIDIA Kepler through Ada Lovelace), the physical Input Assembler is often already implemented using internal vertex-fetch shaders. Consequently, transitioning to manual vertex pulling incurs minimal to no overhead in terms of execution mechanics.

However, manual vertex pulling requires developers to manage cache alignment deliberately. To ensure optimal performance, fetches should be aligned to 4-byte or 16-byte boundaries to maximize throughput on GPU memory controllers. In addition, when relying on indexed draws, developers must manually pull the index from an index buffer before fetching the corresponding vertex data, matching the behavior that fixed-function hardware historically managed automatically.