SDL3: Strange issue when changing texture.

I am putting a game together using SDL3 as it looks excellent; well done to everyone involved.

I have a strange issue when using more than one texture. For example this code:

Transform transform;
transform.set_position(vec2(-2.0f, 0.0f));
transform.set_origin(_cog_sprite->origin());
_draw_list.push_back(std::move(DrawEntry::make(_cog_sprite, transform, 1)));

transform.set_position(vec2(2.0f, 0.0f));
transform.set_origin(_cog_sprite->origin());
_draw_list.push_back(std::move(DrawEntry::make(_cog_sprite, transform, -10)));

works as expected:

However when I use two different sprites, with different textures, then both sprites appear to use the same model matrix:

Transform transform;
transform.set_position(vec2(-2.0f, 0.0f));
transform.set_origin(_cog_sprite->origin());
_draw_list.push_back(std::move(DrawEntry::make(_cog_sprite, transform, 1)));

transform.set_position(vec2(2.0f, 0.0f));
transform.set_origin(_house_sprite->origin());
_draw_list.push_back(std::move(DrawEntry::make(_house_sprite, transform, -10)));

My shaders use an array of sprite uniforms, which just contain the model matrix, and the index into the sprite uniform is passed as a vertex attribute:

cbuffer u0 : register(b0, space1)
{
    float4x4 view_proj;
};

StructuredBuffer<float4x4> sprite_uniforms : register(t0, space0);

SpriteFragIn main(SpriteVertIn input)
{
    SpriteFragIn output;
    float4x4 model = sprite_uniforms[input.uniform_index];
    output.position = mul(view_proj, mul(model, float4(input.position, 0.0f, 1.0f)));
    output.colour = input.colour;
    output.tex_coord = input.tex_coord;
    output.render_flags = input.render_flags;
    return output;
}

At the moment I am using a single render pass to draw all sprites, and once that is complete I am uploading the vertex, index and uniform buffers in a copy pass.

void SpriteRenderer::frame_start(SDL_GPUCommandBuffer* command_buffer,
                                 SDL_GPUTexture* target_texture,
                                 Camera const& camera,
                                 size_t frame) {
    SDL_assert(_render_pass == nullptr);

    _uniform_buffers.reset(frame);
    _vertex_buffers.reset(frame);
    _index_buffers.reset(frame);

    _num_draw_calls = 0;

    // Set the global uniform buffer
    SDL_PushGPUVertexUniformData(command_buffer, 0, &camera.view_proj_matrix(), sizeof(mat4));

    SDL_GPUColorTargetInfo const color_target_info = {
        .texture = target_texture,
        .clear_color = SDL_FColor{ 0.0f, 0.5f, 0.0f, 1.0f },
        .load_op = SDL_GPU_LOADOP_CLEAR,
        .store_op = SDL_GPU_STOREOP_STORE,
    };

    _render_pass = SDL_BeginGPURenderPass(command_buffer, &color_target_info, 1, nullptr);

    SDL_BindGPUGraphicsPipeline(_render_pass, _pipeline.pipeline());

    auto const viewport = camera.viewport();
    SDL_SetGPUViewport(_render_pass, &viewport);

    SDL_GPUBuffer* vertex_storage_buffer = _uniform_buffers.buffer();
    SDL_BindGPUVertexStorageBuffers(_render_pass, 0, &vertex_storage_buffer, 1);

    SDL_GPUBufferBinding const vertex_buffer_binding = {
        .buffer = _vertex_buffers.buffer(),
        .offset = 0,
    };
    SDL_BindGPUVertexBuffers(_render_pass, 0, &vertex_buffer_binding, 1);

    SDL_GPUBufferBinding const index_buffer_binding = {
        .buffer = _index_buffers.buffer(),
        .offset = 0,
    };
    SDL_BindGPUIndexBuffer(_render_pass, &index_buffer_binding, SDL_GPU_INDEXELEMENTSIZE_32BIT);
}

Uint32 SpriteRenderer::draw(DrawListIterator begin, DrawListIterator end) {
    SDL_assert(_render_pass != nullptr);
    ...

    SDL_GPUTextureSamplerBinding const fragment_sampler_binding = {
        .texture = texture,
        .sampler = g_app_state.game->samplers().linear_clamp(),
    };
    SDL_BindGPUFragmentSamplers(_render_pass, 0, &fragment_sampler_binding, 1);

    SDL_DrawGPUIndexedPrimitives(_render_pass, num_sprites * 6, 1, 0, 0, 0);
}

bool SpriteRenderer::frame_end(SDL_GPUCommandBuffer* command_buffer) {
    SDL_assert(_render_pass != nullptr);
    SDL_EndGPURenderPass(_render_pass);
    _render_pass = nullptr;

    auto* copy_pass = SDL_BeginGPUCopyPass(command_buffer);

    if (!_uniform_buffers.upload(copy_pass)) {
        log_error("Failed to upload sprite uniform buffer");
        return false;
    }

    if (!_vertex_buffers.upload(copy_pass)) {
        log_error("Failed to upload sprite vertex buffer");
        return false;
    }

    if (!_index_buffers.upload(copy_pass)) {
        log_error("Failed to upload sprite index buffer");
        return false;
    }

    SDL_EndGPUCopyPass(copy_pass);

    g_app_state.game->timer().profile_draws_add(_num_draw_calls);

    return true;
}

I have debugged the GPU buffer content using the Xcode Metal Debugger and they all appear to be correct.

Vertex buffer:

Index buffer:

Uniform buffer:

Can anyone suggest why the wrong uniform is being used when the texture changes?

Note that this is fixed if I use two uniform buffers; one per-frame and one per-draw. This does cost draw calls, although it probably doesn’t matter and I am interested in knowing why the uniform array approach doesn’t work.

#include "common.hlsli"

cbuffer u0 : register(b0, space1)
{
    float4x4 view_proj;
};

cbuffer u1 : register(b1, space1)
{
    float4x4 model;
};

SpriteFragIn main(SpriteVertIn input)
{
    SpriteFragIn output;
    output.position = mul(view_proj, mul(model, float4(input.position, 0.0f, 1.0f)));
    output.colour = input.colour;
    output.tex_coord = input.tex_coord;
    output.render_flags = input.render_flags;
    return output;
}

This was the issue; the start_index was not being updated with the first index in the batch…

1 Like