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.