Agent skill

forge-vertex-pulling

Add vertex pulling (programmable vertex fetch) to an SDL GPU project. Replace fixed-function vertex input with storage buffer reads in the vertex shader using SV_VertexID and StructuredBuffer.

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/forge-vertex-pulling

SKILL.md

Vertex Pulling with SDL3 GPU

Replace the fixed-function vertex input assembler with manual storage buffer reads in the vertex shader. The pipeline declares zero vertex attributes and the shader fetches vertex data from a StructuredBuffer using SV_VertexID.

Based on GPU Lesson 33 — Vertex Pulling.

When to use

  • Meshes with different vertex layouts sharing one pipeline
  • Compute shaders writing vertex data for the vertex shader to read
  • Compressed or packed vertex formats decoded in the shader
  • GPU-driven rendering with indirect draw calls
  • Reducing pipeline state complexity and pipeline object count

HLSL pattern

Vertex shader (pulled)

hlsl
struct PulledVertex
{
    float3 position;
    float3 normal;
    float2 uv;
};

/* Vertex storage buffer: register(t0, space0) for DXIL vertex shaders.
 * Slot index follows sampled textures and storage textures (if any). */
StructuredBuffer<PulledVertex> vertex_buffer : register(t0, space0);

cbuffer SceneUniforms : register(b0, space1)
{
    column_major float4x4 mvp;
    column_major float4x4 model;
};

struct VSOutput
{
    float4 clip_pos  : SV_Position;
    float3 world_pos : TEXCOORD0;
    float3 world_nrm : TEXCOORD1;
    float2 uv        : TEXCOORD2;
};

VSOutput main(uint vertex_id : SV_VertexID)
{
    VSOutput output;
    PulledVertex v = vertex_buffer[vertex_id];

    float4 world = mul(model, float4(v.position, 1.0));
    output.clip_pos  = mul(mvp, float4(v.position, 1.0));
    output.world_pos = world.xyz;
    output.world_nrm = normalize(mul((float3x3)model, v.normal));
    output.uv        = v.uv;
    return output;
}

Fragment shader

No changes needed — fragment shaders work identically regardless of whether vertex data came from the input assembler or a storage buffer.

C-side pattern

1. Define the vertex struct (must match HLSL)

c
typedef struct PulledVertex {
    vec3 position;   /* 12 bytes */
    vec3 normal;     /* 12 bytes */
    vec2 uv;         /*  8 bytes */
} PulledVertex;      /* 32 bytes */

2. Upload as storage buffer (not vertex buffer)

c
/* The only CPU-side difference: buffer usage flag */
SDL_GPUBuffer *storage_buf = upload_gpu_buffer(
    device,
    SDL_GPU_BUFFERUSAGE_GRAPHICS_STORAGE_READ,  /* NOT VERTEX */
    vertex_data,
    vertex_count * sizeof(PulledVertex));

3. Create shader with storage buffer count

c
/* Vertex shader: declare 1 storage buffer */
SDL_GPUShaderCreateInfo info;
SDL_zero(info);
info.stage               = SDL_GPU_SHADERSTAGE_VERTEX;
info.num_samplers        = 0;
info.num_storage_buffers = 1;   /* <-- vertex data storage buffer */
info.num_uniform_buffers = 1;
/* ... format, code, entrypoint ... */

4. Create pipeline with empty vertex input

c
/* No vertex buffer descriptions, no vertex attributes */
SDL_GPUVertexInputState vis;
SDL_zero(vis);
/* vis.num_vertex_buffers    = 0;  already zero */
/* vis.num_vertex_attributes = 0;  already zero */

SDL_GPUGraphicsPipelineCreateInfo pi;
SDL_zero(pi);
pi.vertex_input_state = vis;
/* ... rest of pipeline setup ... */

5. Bind and draw

c
/* Bind storage buffer instead of vertex buffer */
SDL_GPUBuffer *bufs[1] = { storage_buf };
SDL_BindGPUVertexStorageBuffers(pass, 0, bufs, 1);

/* Index buffer binding is unchanged */
SDL_GPUBufferBinding ib = { index_buffer, 0 };
SDL_BindGPUIndexBuffer(pass, &ib, index_type);
SDL_DrawGPUIndexedPrimitives(pass, index_count, 1, 0, 0, 0);

SDL GPU register mapping

For DXIL vertex shaders, resources are bound in this order at space0:

  1. Sampled textures (t0..tN)
  2. Storage textures (tN+1..)
  3. Storage buffers (tN+M+1..)

With zero sampled/storage textures, the first storage buffer is t0, space0.

For SPIR-V vertex shaders, storage buffers go in descriptor set 0 after sampled and storage textures.

Combining with other techniques

  • Compute → Vertex: Use SDL_GPU_BUFFERUSAGE_GRAPHICS_STORAGE_READ | SDL_GPU_BUFFERUSAGE_COMPUTE_STORAGE_WRITE so a compute shader can write the buffer and the vertex shader can read it.

  • Indirect drawing (Lesson 38): Vertex pulling pairs naturally with indirect draws for fully GPU-driven rendering.

  • Multiple vertex formats: One pipeline can render meshes with different vertex layouts by defining multiple StructuredBuffer structs or using a ByteAddressBuffer with manual offset calculations.

Common mistakes

Mistake Fix
Using SDL_GPU_BUFFERUSAGE_VERTEX Use SDL_GPU_BUFFERUSAGE_GRAPHICS_STORAGE_READ
Forgetting num_storage_buffers = 1 on shader Set it in SDL_GPUShaderCreateInfo
Calling SDL_BindGPUVertexBuffers Use SDL_BindGPUVertexStorageBuffers
Mismatched C/HLSL struct layout Ensure identical field order and sizes
Leaving vertex attributes in pipeline Set num_vertex_buffers = 0, num_vertex_attributes = 0

Expand your agent's capabilities with these related and highly-rated skills.

Didn't find tool you were looking for?

Be as detailed as possible for better results