Shadow Pass requires BindGPUFragmentSamplers

Hello I’ve been writing a 3D renderer using SDL3’s new GPU api. I used the examples to get started and I’ve adapted that to do a depth only shadow map where I render the scene from the lights perspective (direction light for now). I’ve noticed I get a crash with the error “Missing fragment sampler binding!” while calling SDL_DrawGPUIndexedPrimitives in the shadow pass. If I call SDL_BindGPUFragmentSamplers with a dummy texture and a sampler everything seems to work fine. I’m just wondering what that call is doing and why it works, or if there is a better way to fix the error.

Here is an excerpt of my code for what I think are the relevant parts, the pass only has a vertex shader that just does the matrix projection math from the lights point of view.

// Shadow Pipeline setup
SDL_GPUTexture* shadowTexture = SDL_CreateGPUTexture(
    device,
    &(SDL_GPUTextureCreateInfo) {
        .type = SDL_GPU_TEXTURETYPE_2D,
        .width = 1024,
        .height = 1024,
        .layer_count_or_depth = 1,
        .num_levels = 1,
        .sample_count = SDL_GPU_SAMPLECOUNT_1,
        .format = SDL_GPU_TEXTUREFORMAT_D16_UNORM,
        .usage = SDL_GPU_TEXTUREUSAGE_SAMPLER | SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET
    }
);

SDL_GPUTexture* dummyTexture = SDL_CreateGPUTexture(
    device,
    &(SDL_GPUTextureCreateInfo) {
        .type = SDL_GPU_TEXTURETYPE_2D,
        .width = 8,
        .height = 8,
        .layer_count_or_depth = 1,
        .num_levels = 1,
        .sample_count = SDL_GPU_SAMPLECOUNT_1,
        .format = SDL_GPU_TEXTUREFORMAT_D16_UNORM,
        .usage = SDL_GPU_TEXTUREUSAGE_SAMPLER | SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET
    }
);

SDL_GPUSampler* shadowSampler = SDL_CreateGPUSampler(device, &(SDL_GPUSamplerCreateInfo){
    .min_filter = SDL_GPU_FILTER_NEAREST,
    .mag_filter = SDL_GPU_FILTER_NEAREST,
    .mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST,
    .address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE,
    .address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE,
    .address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE,
    .compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL,
    .enable_compare = true,
});

SDL_GPUShader* shadowVertexShader = LoadShader(device, "shadow.vert", 1, 2, 0, 0);
if (shadowVertexShader == nullptr)
{
    SDL_Log("Failed to create shadow vertex shader!");
    return -1;
}

SDL_GPUGraphicsPipelineCreateInfo shadowPipelineCreateInfo = {
    .target_info = {
        .num_color_targets = 0,
        .has_depth_stencil_target = true,
        .depth_stencil_format = SDL_GPU_TEXTUREFORMAT_D16_UNORM
    },
    .rasterizer_state = (SDL_GPURasterizerState){
        .cull_mode = SDL_GPU_CULLMODE_BACK,
        .fill_mode = SDL_GPU_FILLMODE_FILL,
        .front_face = SDL_GPU_FRONTFACE_CLOCKWISE,
        .enable_depth_bias = true,
        .enable_depth_clip = true,
    },
    .depth_stencil_state = (SDL_GPUDepthStencilState){
        .enable_depth_test = true,
        .enable_depth_write = true,
        .enable_stencil_test = false,
        .compare_op = SDL_GPU_COMPAREOP_LESS,
        .write_mask = 0xFF
    },
    .vertex_input_state = (SDL_GPUVertexInputState){
        .num_vertex_buffers = 1,
        .vertex_buffer_descriptions = (SDL_GPUVertexBufferDescription[]){{
            .slot = 0,
            .input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX,
            .instance_step_rate = 0,
            .pitch = sizeof(PositionNormalVertex)
        }},
        .num_vertex_attributes = 3,
        .vertex_attributes = (SDL_GPUVertexAttribute[]){{
            .buffer_slot = 0,
            .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3,
            .location = 0,
            .offset = 0
        }, {
            .buffer_slot = 0,
            .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3,
            .location = 1,
            .offset = sizeof(float) * 3
        }, {
            .buffer_slot = 0,
            .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2,
            .location = 2,
            .offset = sizeof(float) * 6
        }}
    },
    .primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST,
    .vertex_shader = shadowVertexShader,
};

SDL_GPUGraphicsPipeline* shadowPipeline = SDL_CreateGPUGraphicsPipeline(device, &pipelineCreateInfo);
if (shadowPipeline == NULL)
{
    SDL_Log("Failed to create vertext buffer pipeline!");
    return -1;
}


/////////////////////////////////////////////////////////////////////////////////
// ... Loading of textures and models and other data
/////////////////////////////////////////////////////////////////////////////////


// In my main loop, before the render pass
SDL_GPUDepthStencilTargetInfo shadowMapTargetInfo = {0};
shadowMapTargetInfo.texture = shadowTexture;
shadowMapTargetInfo.cycle = true;
shadowMapTargetInfo.clear_depth = 1;
shadowMapTargetInfo.clear_stencil = 0;
shadowMapTargetInfo.load_op = SDL_GPU_LOADOP_CLEAR;
shadowMapTargetInfo.store_op = SDL_GPU_STOREOP_STORE;
shadowMapTargetInfo.stencil_load_op = SDL_GPU_LOADOP_CLEAR;
shadowMapTargetInfo.stencil_store_op = SDL_GPU_STOREOP_STORE;

SDL_GPURenderPass* shadowPass = SDL_BeginGPURenderPass(cmdbuf, NULL, 0, &shadowMapTargetInfo);
SDL_GPUViewport shadow_viewport = {0, 0, 1024, 1024, 0.0, 1.f}; 
SDL_SetGPUViewport(shadowPass, &shadow_viewport);
SDL_BindGPUGraphicsPipeline(shadowPass, shadowPipeline);
SDL_BindGPUVertexBuffers(shadowPass, 0, &(SDL_GPUBufferBinding){.buffer = cube->vertexBuffer, .offset = 0}, 1);
// TODO: why does this need to be bound?
SDL_BindGPUFragmentSamplers(shadowPass, 0, &(SDL_GPUTextureSamplerBinding){ .texture = dummyTexture, .sampler = shadowSampler}, 1);
SDL_BindGPUIndexBuffer(shadowPass, &(SDL_GPUBufferBinding){ .buffer = cube->indexBuffer, .offset = 0}, SDL_GPU_INDEXELEMENTSIZE_16BIT);
                                                                                                                                         
SDL_PushGPUVertexUniformData(cmdbuf, 0, &lightSpaceMatrix, sizeof(lightSpaceMatrix));
SDL_PushGPUVertexUniformData(cmdbuf, 1, &model, sizeof(model));
                                                                                                                                         
SDL_DrawGPUIndexedPrimitives(shadowPass, cube->indexCount, 1, 0, 0 ,0);
                                                                                                                                         
SDL_BindGPUVertexBuffers(shadowPass, 0, &(SDL_GPUBufferBinding){.buffer = floor->vertexBuffer, .offset = 0}, 1);
SDL_BindGPUIndexBuffer(shadowPass, &(SDL_GPUBufferBinding){ .buffer = floor->indexBuffer, .offset = 0}, SDL_GPU_INDEXELEMENTSIZE_16BIT);
                                                                                                                                         
SDL_PushGPUVertexUniformData(cmdbuf, 0, &lightSpaceMatrix, sizeof(lightSpaceMatrix));
SDL_PushGPUVertexUniformData(cmdbuf, 1, &floorTrans, sizeof(floorTrans));
                                                                                                                                         
SDL_DrawGPUIndexedPrimitives(shadowPass, floor->indexCount, 1, 0, 0 ,0);

SDL_EndGPURenderPass(shadowPass);

Thanks for the help.

I’m dumb, when I call SDL_CreateGPUGraphicsPipeline in the code above I’m using the pipeline create info for my renderer not the one I just created for the shadow…