Glusoft

Display a tilemap from Tiled with SDL3

Tilemap's result

Configure the project using CMake

For the Cmakelist we will need to have SDL3 and SDL3_image, we will put every images and shaders in the folder assets.
The folder contains multiple file so we need to find all the soource files using glob:

cmake_minimum_required(VERSION 3.16)
project(SDL3_Tilemap)

# Set C++ standard
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
file(GLOB ASSETS "assets/*")

file(COPY ${ASSETS} DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/assets)

find_package(SDL3 REQUIRED)
find_package(SDL3_image REQUIRED)

file(GLOB SOURCES "src/*.cpp" "src/*.hpp")

add_executable(SDL3_Tilemap ${SOURCES})

target_include_directories(SDL3_Tilemap PRIVATE ${CMAKE_SOURCE_DIR}/rapidjson/include)
target_link_libraries(SDL3_Tilemap PRIVATE SDL3::SDL3 SDL3_image::SDL3_image)

Loading the tileset

To do display a tilemap we first need to load the tileset. To store the data of the tileset we need a struct:

struct Tile {
    int id;
    SDL_FRect rect;
};

struct Tileset {
    std::string name;
    std::string imagePath;
    SDL_Surface *surface = nullptr;
    int imageWidth;
    int imageHeight;
    int columns;
    int tileWidth;
    int tileHeight;
    int tilecount;
    std::vector<Tile> tiles;
    
    void load();
    void parseTileset(const std::string& filename);
};
We need to explicit the load function responsible of creating the tiles and the parsesing function parseTileset reponsible fo filling the struct with the other data.
To parse the json we can use the library rapidjson.

Here is the code for the parse function :
void parseTileset(const std::string& filename) {
    std::ifstream ifs(filename);
    if (!ifs.is_open()) {
        std::cerr << "Error opening file: " << filename << std::endl;
        return;
    }

    rapidjson::IStreamWrapper isw(ifs);
    rapidjson::Document doc;
    doc.ParseStream(isw);

    if (doc.HasParseError()) {
        std::cerr << "JSON parse error!" << std::endl;
        return;
    }

    // Extract top-level values
    columns = doc["columns"].GetInt();
    imagePath = doc["image"].GetString();
    imageHeight = doc["imageheight"].GetInt();
    imageWidth = doc["imagewidth"].GetInt();
    name = doc["name"].GetString();
    tileWidth = doc["tilewidth"].GetInt();
    tileHeight = doc["tileheight"].GetInt();
    tilecount = doc["tilecount"].GetInt();
    
    load();
    
    //std::cout << "Tileset Name: " << tilesetName << std::endl;
    //std::cout << "Image: " << image << " (" << imageWidth << "x" << imageHeight << ")" << std::endl;
    //std::cout << "Columns: " << columns << ", Tile Size: " << tileWidth << "x" << tileHeight << std::endl;

    // Parsing the "tiles" array
    if (doc.HasMember("tiles") && doc["tiles"].IsArray()) {
        const rapidjson::Value& tiles = doc["tiles"];

        for (const auto& tile : tiles.GetArray()) {
            int tileID = tile["id"].GetInt();
            std::cout << "\nTile ID: " << tileID << std::endl;

            if (tile.HasMember("properties") && tile["properties"].IsArray()) {
                for (const auto& property : tile["properties"].GetArray()) {
                    std::string propName = property["name"].GetString();
                    std::string propType = property["type"].GetString();
                    std::string propValue = property["value"].GetString();

                    std::cout << "  Property: " << propName << " (" << propType << ") = " << propValue << std::endl;
                }
            }
        }
    }
}

In tiled tiles can have properties in this tutorial we do not make use of this.
For the load function:

void load() {
    std::string path = "../assets/"+imagePath;
    surface = IMG_Load(path.c_str());
    
    if (!surface) {
        std::cerr << "SDL_image Load Error: " << SDL_GetError() << std::endl;
        return;
    }
    
    int numRows = imageHeight/tileHeight;
    int numCols = columns;
    
    tiles.reserve(tilecount);
    
    for(int i = 0; i < numRows; i++) {
        for(int j = 0; j < numCols; j++) {
            Tile tile { i*numCols + j,
                SDL_FRect{
                    static_cast<float>(j*tileWidth),
                    static_cast<float>(i*tileHeight),
                    static_cast<float>(tileWidth),
                    static_cast<float>(tileHeight)
                }
            };
            tiles.emplace_back(tile);
        }
    }
    
    std::cout << tiles.size() << std::endl;
}

Load the tilemap

After loading the tileset we need to load the tilemap, we will create a struct:

struct TileMap {
    int compressionlevel;
    int height;
    bool infinite;
    std::vector<std::vector<int>> layers;

    int parseTilemap(std::string path);
};

For the parseTilemap function we still use rapidjson:

int parseTilemap(std::string path) {
    // Step 1: Read the JSON file into a string
    std::ifstream file(path);
    if (!file) {
        std::cerr << "Error opening file!" << std::endl;
        return 1;
    }

    std::stringstream buffer;
    buffer << file.rdbuf();
    std::string jsonData = buffer.str();

    // Step 2: Parse the JSON string
    Document document;
    document.Parse(jsonData.c_str());

    // Step 3: Check if parsing was successful
    if (document.HasParseError()) {
        std::cerr << "Error parsing JSON!" << std::endl;
        return 1;
    }

    // Step 4: Access JSON fields
    if (document.HasMember("compressionlevel") && document["compressionlevel"].IsInt()) {
        compressionlevel = document["compressionlevel"].GetInt();
    }

    if (document.HasMember("height") && document["height"].IsInt()) {
        height = document["height"].GetInt();
    }

    if (document.HasMember("infinite") && document["infinite"].IsBool()) {
        infinite = (document["infinite"].GetBool() ? "true" : "false");
    }

    // Step 5: Accessing the layers and data
    if (document.HasMember("layers") && document["layers"].IsArray()) {
        const Value& layersDoc = document["layers"];
        std::cout << "Number of layers: " << layersDoc.Size() << std::endl;
        layers.reserve(layersDoc.Size());
        
        for (SizeType i = 0; i < layersDoc.Size(); i++) {
            if (layersDoc[i].HasMember("data") && layersDoc[i]["data"].IsArray()) {
                const Value& data = layersDoc[i]["data"];
                std::vector<int> layer_in;
                layer_in.reserve(data.Size());
                
                for (SizeType j = 0; j < data.Size(); j++) {
                    layer_in.emplace_back(data[j].GetInt());
                }
                layers.emplace_back(layer_in);
            }
        }
    }
    
    std::cout << "size:" << layers[0].size() << std::endl;

    return 0;
}

Create context for sprite batching

To enable sprite batching we need to do the same as the previous tutorial about Sprite batching:

static SDL_GPUGraphicsPipeline* RenderPipeline;
static SDL_GPUSampler* Sampler;
static SDL_GPUTexture* Texture;
static SDL_GPUTransferBuffer* SpriteDataTransferBuffer;
static SDL_GPUBuffer* SpriteDataBuffer;
static const Uint32 SPRITE_COUNT = 10576;
static const char* BasePath = NULL;

typedef struct SpriteInstance
{
    float x, y, z;
    float rotation;
    float w, h, padding_a, padding_b;
    float tex_u, tex_v, tex_w, tex_h;
    float r, g, b, a;
} SpriteInstance;

typedef struct Matrix4x4
{
    float m11, m12, m13, m14;
    float m21, m22, m23, m24;
    float m31, m32, m33, m34;
    float m41, m42, m43, m44;
} Matrix4x4;

Matrix4x4 Matrix4x4_CreateOrthographicOffCenter(
    float left,
    float right,
    float bottom,
    float top,
    float zNearPlane,
    float zFarPlane
) {
    return (Matrix4x4) {
        2.0f / (right - left), 0, 0, 0,
        0, 2.0f / (top - bottom), 0, 0,
        0, 0, 1.0f / (zNearPlane - zFarPlane), 0,
        (left + right) / (left - right), (top + bottom) / (bottom - top), zNearPlane / (zNearPlane - zFarPlane), 1
    };
}

For loading the shader:

SDL_GPUShader* LoadShader(
    SDL_GPUDevice* device,
    const char* shaderFilename,
    Uint32 samplerCount,
    Uint32 uniformBufferCount,
    Uint32 storageBufferCount,
    Uint32 storageTextureCount
) {
    // Auto-detect the shader stage from the file name for convenience
    SDL_GPUShaderStage stage;
    if (SDL_strstr(shaderFilename, ".vert"))
    {
        stage = SDL_GPU_SHADERSTAGE_VERTEX;
    }
    else if (SDL_strstr(shaderFilename, ".frag"))
    {
        stage = SDL_GPU_SHADERSTAGE_FRAGMENT;
    }
    else
    {
        SDL_Log("Invalid shader stage!");
        return NULL;
    }

    char fullPath[256];
    SDL_GPUShaderFormat backendFormats = SDL_GetGPUShaderFormats(device);
    SDL_GPUShaderFormat format = SDL_GPU_SHADERFORMAT_INVALID;
    const char *entrypoint;

    if (backendFormats & SDL_GPU_SHADERFORMAT_SPIRV) {
        SDL_snprintf(fullPath, sizeof(fullPath), "%s/%s.spv", BasePath, shaderFilename);
        format = SDL_GPU_SHADERFORMAT_SPIRV;
        entrypoint = "main";
    } else if (backendFormats & SDL_GPU_SHADERFORMAT_MSL) {
        SDL_snprintf(fullPath, sizeof(fullPath), "%s/%s.msl", BasePath, shaderFilename);
        format = SDL_GPU_SHADERFORMAT_MSL;
        entrypoint = "main0";
    } else if (backendFormats & SDL_GPU_SHADERFORMAT_DXIL) {
        SDL_snprintf(fullPath, sizeof(fullPath), "%s/%s.dxil", BasePath, shaderFilename);
        format = SDL_GPU_SHADERFORMAT_DXIL;
        entrypoint = "main";
    } else {
        SDL_Log("%s", "Unrecognized backend shader format!");
        return NULL;
    }

    size_t codeSize;
    void* code = SDL_LoadFile(fullPath, &codeSize);
    if (code == NULL)
    {
        SDL_Log("Failed to load shader from disk! %s", fullPath);
        return NULL;
    }

    SDL_GPUShaderCreateInfo shaderInfo = {
        .code =  (Uint8*)code,
        .code_size = codeSize,
        .entrypoint = entrypoint,
        .format = format,
        .stage = stage,
        .num_samplers = samplerCount,
        .num_uniform_buffers = uniformBufferCount,
        .num_storage_buffers = storageBufferCount,
        .num_storage_textures = storageTextureCount
    };
    SDL_GPUShader* shader = SDL_CreateGPUShader(device, &shaderInfo);
    if (shader == NULL)
    {
        SDL_Log("Failed to create shader!");
        SDL_free(code);
        return NULL;
    }

    SDL_free(code);
    return shader;
}

For loading the image:

SDL_Surface* LoadImage(const char* imageFilename, int desiredChannels)
{
    char fullPath[256];
    SDL_Surface *result;
    SDL_PixelFormat format;

    SDL_snprintf(fullPath, sizeof(fullPath), "%s/%s", BasePath, imageFilename);

    result = IMG_Load(fullPath);
    if (result == NULL)
    {
        SDL_Log("Failed to load BMP: %s", SDL_GetError());
        return NULL;
    }

    if (desiredChannels == 4)
    {
        format = SDL_PIXELFORMAT_ABGR8888;
    }
    else
    {
        SDL_assert(!"Unexpected desiredChannels");
        SDL_DestroySurface(result);
        return NULL;
    }
    if (result->format != format)
    {
        SDL_Surface *next = SDL_ConvertSurface(result, format);
        SDL_DestroySurface(result);
        result = next;
    }

    return result;
}

The main function

We need to call everything we have created so far:

#define WIDTH 800
#define HEIGHT 600

int main(int argc, char* argv[])
{
    SDL_Init(SDL_INIT_VIDEO);
    
    BasePath = SDL_GetBasePath();

    // Create SDL Window
    SDL_Window* window = SDL_CreateWindow("SDL3 Tilemap Game", WIDTH, HEIGHT, SDL_WINDOW_VULKAN  | SDL_WINDOW_RESIZABLE);
    if (!window) {
        std::cerr << "Failed to create window: " << SDL_GetError() << std::endl;
        return -1;
    }

    // Create Renderer
    SDL_Renderer* renderer = SDL_CreateRenderer(window, NULL);
    if (!renderer) {
        std::cerr << "Failed to create renderer: " << SDL_GetError() << std::endl;
        SDL_DestroyWindow(window);
        return -1;
    }
    
    SDL_GPUDevice* device = SDL_CreateGPUDevice(
                                                SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_DXIL | SDL_GPU_SHADERFORMAT_MSL,
                                                false,
                                                NULL);

    if (!device)
    {
        printf("SDL_CreateGPUDevice failed: %s\n", SDL_GetError());
        SDL_DestroyWindow(window);
        SDL_Quit();
        return -1;
    }
    
    if (!SDL_ClaimWindowForGPUDevice(device, window))
    {
        SDL_Log("GPUClaimWindow failed");
        return -1;
    }
    
    SDL_GPUPresentMode presentMode = SDL_GPU_PRESENTMODE_VSYNC;
    if (SDL_WindowSupportsGPUPresentMode(
        device,
        window,
        SDL_GPU_PRESENTMODE_IMMEDIATE
    )) {
        presentMode = SDL_GPU_PRESENTMODE_IMMEDIATE;
    }
    else if (SDL_WindowSupportsGPUPresentMode(
        device,
        window,
        SDL_GPU_PRESENTMODE_MAILBOX
    )) {
        presentMode = SDL_GPU_PRESENTMODE_MAILBOX;
    }

    SDL_SetGPUSwapchainParameters(
        device,
        window,
        SDL_GPU_SWAPCHAINCOMPOSITION_SDR,
        presentMode
    );

    // Create the shaders
    SDL_GPUShader* vertShader = LoadShader(
        device,
        "../assets/PullSpriteBatch.vert",
        0,
        1,
        1,
        0
    );

    SDL_GPUShader* fragShader = LoadShader(
        device,
        "../assets/TexturedQuadColor.frag",
        1,
        0,
        0,
        0
    );
    
    SDL_GPUColorTargetDescription target_desc[] {{
        .format = SDL_GetGPUSwapchainTextureFormat(device, window),
        .blend_state = {
            .enable_blend = true,
            .color_blend_op = SDL_GPU_BLENDOP_ADD,
            .alpha_blend_op = SDL_GPU_BLENDOP_ADD,
            .src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA,
            .dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
            .src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA,
            .dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
        }
    }};
        
    SDL_GPUGraphicsPipelineTargetInfo pipeline_target_info{
        .num_color_targets = 1,
        .color_target_descriptions = target_desc
    };
    
    SDL_GPUGraphicsPipelineCreateInfo pipeline_info{
        .target_info = pipeline_target_info,
        .primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST,
        .vertex_shader = vertShader,
        .fragment_shader = fragShader
    };

    // Create the sprite render pipeline
    RenderPipeline = SDL_CreateGPUGraphicsPipeline(
        device,
        &pipeline_info
    );

    SDL_ReleaseGPUShader(device, vertShader);
    SDL_ReleaseGPUShader(device, fragShader);
    
    Tileset tileset;
    tileset.parseTileset("../assets/alienor.json");

    TileMap tilemap;
    tilemap.parseTilemap("../assets/map1_test1.json");

    // Load the image data
    SDL_Surface *imageData = LoadImage("../assets/alienor.png", 4);
    if (imageData == NULL)
    {
        SDL_Log("Could not load image data!");
        return -1;
    }
    
    SDL_GPUTransferBufferCreateInfo buffer_info {
        .usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD,
        .size = static_cast<Uint32>(imageData->w * imageData->h * 4)
    };

    SDL_GPUTransferBuffer* textureTransferBuffer = SDL_CreateGPUTransferBuffer(
        device,
        &buffer_info
    );

    Uint8 *textureTransferPtr = (Uint8*) SDL_MapGPUTransferBuffer(
        device,
        textureTransferBuffer,
        false
    );
    SDL_memcpy(textureTransferPtr, imageData->pixels, imageData->w * imageData->h * 4);
    SDL_UnmapGPUTransferBuffer(device, textureTransferBuffer);

    SDL_GPUTextureCreateInfo texture_info{
        .type = SDL_GPU_TEXTURETYPE_2D,
        .format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM,
        .width = static_cast<Uint32>(imageData->w),
        .height = static_cast<Uint32>(imageData->h),
        .layer_count_or_depth = 1,
        .num_levels = 1,
        .usage = SDL_GPU_TEXTUREUSAGE_SAMPLER
    };
    
    // Create the GPU resources
    Texture = SDL_CreateGPUTexture(
        device,
        &texture_info
    );
    
    SDL_GPUSamplerCreateInfo sampler_info {
        .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
    };
    
    Sampler = SDL_CreateGPUSampler(
        device,
        &sampler_info
    );
    
    SDL_GPUTransferBufferCreateInfo transfer_buffer_info {
        .usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD,
        .size = SPRITE_COUNT * sizeof(SpriteInstance)
    };
    
    SpriteDataTransferBuffer = SDL_CreateGPUTransferBuffer(
        device,
        &transfer_buffer_info
    );

    SDL_GPUBufferCreateInfo buffer_info_read {
        .usage = SDL_GPU_BUFFERUSAGE_GRAPHICS_STORAGE_READ,
        .size = SPRITE_COUNT * sizeof(SpriteInstance)
    };
    
    SpriteDataBuffer = SDL_CreateGPUBuffer(
        device,
        &buffer_info_read
    );

    // Transfer the up-front data
    SDL_GPUCommandBuffer* uploadCmdBuf = SDL_AcquireGPUCommandBuffer(device);
    SDL_GPUCopyPass* copyPass = SDL_BeginGPUCopyPass(uploadCmdBuf);

    SDL_GPUTextureTransferInfo transfer_info {
        .transfer_buffer = textureTransferBuffer,
        .offset = 0, /* Zeroes out the rest */
    };

    SDL_GPUTextureRegion texture_region {
        .texture = Texture,
        .w = static_cast<Uint32>(imageData->w),
        .h = static_cast<Uint32>(imageData->h),
        .d = 1
    };
    
    SDL_UploadToGPUTexture(
        copyPass,
        &transfer_info,
        &texture_region,
        false
    );
    
    SDL_GPUTextureSamplerBinding sampler_binding {
        .texture = Texture,
        .sampler = Sampler
    };

    SDL_EndGPUCopyPass(copyPass);
    SDL_SubmitGPUCommandBuffer(uploadCmdBuf);

    SDL_DestroySurface(imageData);
    SDL_ReleaseGPUTransferBuffer(device, textureTransferBuffer);

    bool running = true;
    SDL_Event event;
    
    static float uCoords[4] = { 0.0f, 0.5f, 0.0f, 0.5f };
    static float vCoords[4] = { 0.0f, 0.0f, 0.5f, 0.5f };
    
    float factorX = 1;
    float factorY = 1;

    while (running)
    {
        // Handle events
        while (SDL_PollEvent(&event))
        {
            if (event.type == SDL_EVENT_QUIT)
            {
                running = false;
            }
            if(event.type == SDL_EVENT_KEY_DOWN) {
                if(event.key.key == SDLK_A) {
                    factorX=factorX-1.f;
                }
                if(event.key.key == SDLK_Z) {
                    factorX=factorX+1.f;
                }
                if(event.key.key == SDLK_Q) {
                    factorY=factorY-1.f;
                }
                if(event.key.key == SDLK_S) {
                    factorY=factorY+1.f;
                }
            }
        }

        render(device, tilemap, tileset, window);
    }

    // Cleanup
    SDL_ReleaseGPUGraphicsPipeline(device, RenderPipeline);
    SDL_ReleaseGPUSampler(device, Sampler);
    SDL_ReleaseGPUTexture(device, Texture);
    SDL_ReleaseGPUTransferBuffer(device, SpriteDataTransferBuffer);
    SDL_ReleaseGPUBuffer(device, SpriteDataBuffer);
    
    return 0;
}

The render function

The main look like the previous example except for the loading of the tileset and tilemap and the render function doing the mapping between the tilemap and the buffer for the sprite batching.
Here is the render function:

static int render(SDL_GPUDevice *device, TileMap &tilemap, Tileset &tileset, SDL_Window *window) {
    Matrix4x4 cameraMatrix = Matrix4x4_CreateOrthographicOffCenter(
                                                                   0,
                                                                   640,
                                                                   480,
                                                                   0,
                                                                   0,
                                                                   -1
                                                                   );
    
    SDL_GPUCommandBuffer* cmdBuf = SDL_AcquireGPUCommandBuffer(device);
    if (cmdBuf == NULL)
    {
        SDL_Log("AcquireGPUCommandBuffer failed: %s", SDL_GetError());
        return -1;
    }
    
    SDL_GPUTexture* swapchainTexture;
    if (!SDL_WaitAndAcquireGPUSwapchainTexture(cmdBuf, window, &swapchainTexture, NULL, NULL)) {
        SDL_Log("WaitAndAcquireGPUSwapchainTexture failed: %s", SDL_GetError());
        return -1;
    }
    
    if (swapchainTexture != NULL)
    {
        // Build sprite instance transfer
        SpriteInstance* dataPtr = (SpriteInstance*)SDL_MapGPUTransferBuffer(
                                                                            device,
                                                                            SpriteDataTransferBuffer,
                                                                            true
                                                                            );
        
        int numCols = 100;
        int numRows = 100;
        
        for (int j = 0; j < numRows; j++) { // Iterate over rows first
            for (int i = 0; i < numCols; i++) { // Then iterate over columns
                int index = j * numCols + i;  // Correct row-major indexing
                int n = tilemap.layers[0][index];  // Tile index in tileset
                
                // Position in world space
                dataPtr[index].x = i * tileset.tileWidth;
                dataPtr[index].y = j * tileset.tileHeight;
                dataPtr[index].z = 0;
                dataPtr[index].rotation = 0;
                
                // Tile dimensions
                dataPtr[index].w = tileset.tileWidth;
                dataPtr[index].h = tileset.tileHeight;
                
                // Get tile rect from tileset
                float tex_x = tileset.tiles[n-1].rect.x;
                float tex_y = tileset.tiles[n-1].rect.y;
                float tex_w = tileset.tileWidth;
                float tex_h = tileset.tileHeight;
                float tex_w_max = tex_w;
                float tex_h_max = tex_h;
                
                // Convert to normalized UV coordinates
                dataPtr[index].tex_u = (tex_x / tileset.imageWidth);
                dataPtr[index].tex_v = (tex_y / tileset.imageHeight);
                dataPtr[index].tex_w = (tex_w_max / tileset.imageWidth);
                dataPtr[index].tex_h = (tex_h_max / tileset.imageHeight);
                
                // Default colors (white)
                dataPtr[index].r = 1.0f;
                dataPtr[index].g = 1.0f;
                dataPtr[index].b = 1.0f;
                dataPtr[index].a = 1.0f;
            }
        }
        
        
        SDL_UnmapGPUTransferBuffer(device, SpriteDataTransferBuffer);
        
        SDL_GPUTransferBufferLocation buffer_location {
            .transfer_buffer = SpriteDataTransferBuffer,
            .offset = 0
        };
        
        SDL_GPUBufferRegion buffer_region {
            .buffer = SpriteDataBuffer,
            .offset = 0,
            .size = SPRITE_COUNT * sizeof(SpriteInstance)
        };
        
        // Upload instance data
        SDL_GPUCopyPass* copyPass = SDL_BeginGPUCopyPass(cmdBuf);
        SDL_UploadToGPUBuffer(
                              copyPass,
                              &buffer_location,
                              &buffer_region,
                              true
                              );
        SDL_EndGPUCopyPass(copyPass);
        
        SDL_GPUColorTargetInfo target_info{
            .texture = swapchainTexture,
            .cycle = false,
            .load_op = SDL_GPU_LOADOP_CLEAR,
            .store_op = SDL_GPU_STOREOP_STORE,
            .clear_color = { 0, 0, 0, 1 }
        };
        
        // Render sprites
        SDL_GPURenderPass* renderPass = SDL_BeginGPURenderPass(
                                                               cmdBuf,
                                                               &target_info,
                                                               1,
                                                               NULL
                                                               );
        
        SDL_BindGPUGraphicsPipeline(renderPass, RenderPipeline);
        SDL_BindGPUVertexStorageBuffers(
                                        renderPass,
                                        0,
                                        &SpriteDataBuffer,
                                        1
                                        );
        SDL_GPUTextureSamplerBinding sampler_binding{
            .texture = Texture,
            .sampler = Sampler
        };
        SDL_BindGPUFragmentSamplers(
                                    renderPass,
                                    0,
                                    &sampler_binding,
                                    1
                                    );
        SDL_PushGPUVertexUniformData(
                                     cmdBuf,
                                     0,
                                     &cameraMatrix,
                                     sizeof(Matrix4x4)
                                     );
        SDL_DrawGPUPrimitives(
                              renderPass,
                              SPRITE_COUNT * 6,
                              1,
                              0,
                              0
                              );
        
        SDL_EndGPURenderPass(renderPass);
    }
    
    SDL_SubmitGPUCommandBuffer(cmdBuf);
    
    
    // Present the frame
    SDL_GL_SwapWindow(window);
}

Full project

You can download the full project:

  • For macos: SDL3_Tilemap_mac.7z
  • For windows you can download this file SDL3_Tilemap_win.7z: