Welcome to collectivesolver - Programming & Software Q&A with code examples. A website with trusted programming answers. All programs are tested and work.

Contact: aviboots(AT)netvision.net.il

Semrush - keyword research tool

Turn ChatGPT, Claude, Gemini, And CoPilot Into Your Personal Assistant, Business Coach, Content Creator, And More

AFFILIATE MARKETING Your all-in-one performance engine Manage affiliates, creators, and customer referrals in one unified platform—turning every partnership into measurable growth
Secure & Reliable Web Hosting, Free Domain, Free SSL, 1-Click WordPress Install, Expert 24/7 Support

Boost your online presence with premium web hosting and servers

Disclosure: My content contains affiliate links.

42,753 questions

55,518 answers

573 users

How to render a triangle using uniform buffer with SDL_GPU in C++

1 Answer

0 votes
#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL_main.h>
#include <SDL3/SDL.h>

// Vertex Definition 
// ----------------- 

// A vertex contains a 3D position and an RGBA color.
// This matches the layout expected by the vertex shader.
struct Vertex
{
    float x, y, z;      // Position (vec3)
    float r, g, b, a;   // Color    (vec4)
};

// Three vertices forming a triangle.
// Each vertex has a unique color → fragment shader interpolates colors.
static Vertex vertices[]
{
    { 0.0f,  0.5f, 0.0f, 1, 0, 0, 1 },   // Top (red)
    {-0.5f, -0.5f, 0.0f, 1, 1, 0, 1 },   // Bottom-left (yellow)
    { 0.5f, -0.5f, 0.0f, 0, 0, 1, 1 }    // Bottom-right (blue)
};

// Uniform buffers = set universal properties that can be set before the draw call.
// Uniform buffers accessible to all vertices in that call.

struct UniformBuffer
{
    float time;
};

static UniformBuffer timeUniform{};

SDL_Window* window;
SDL_GPUDevice* device;
SDL_GPUBuffer* vertexBuffer;
SDL_GPUTransferBuffer* transferBuffer;
SDL_GPUGraphicsPipeline* graphicsPipeline;

// SDL_AppInit — Initialization Phase 
// ---------------------------------- 

SDL_AppResult SDL_AppInit(void** appstate, int argc, char** argv)
{
    // create a window
    window = SDL_CreateWindow("SDL_GPU - Create Triangle", 960, 540, SDL_WINDOW_RESIZABLE);

    // Create GPU device using SPIR-V shaders
    device = SDL_CreateGPUDevice(SDL_GPU_SHADERFORMAT_SPIRV, true, NULL); // Vulkan - cross-platform

    // Attach window to GPU device (swapchain creation)
    SDL_ClaimWindowForGPUDevice(device, window);

    // Load & Create Shaders 
    // --------------------- 
    
    // Vertex Shader 
    // -------------
    // 
    // load the vertex shader code
    size_t vertexCodeSize;
    // compile the shader in commant prompt:
    // glslc -fshader-stage=vertex shaders/vertex.glsl -o shaders/vertex.spv
    void* vertexCode = SDL_LoadFile("shaders/vertex.spv", &vertexCodeSize);

    // create the vertex shader
    SDL_GPUShaderCreateInfo vertexInfo{};
    vertexInfo.code = (Uint8*)vertexCode;
    vertexInfo.code_size = vertexCodeSize;
    vertexInfo.entrypoint = "main"; // SPIR-V entry point
    vertexInfo.format = SDL_GPU_SHADERFORMAT_SPIRV;
    vertexInfo.stage = SDL_GPU_SHADERSTAGE_VERTEX;
    vertexInfo.num_samplers = 0;
    vertexInfo.num_storage_buffers = 0;
    vertexInfo.num_storage_textures = 0;
    vertexInfo.num_uniform_buffers = 0;

    SDL_GPUShader* vertexShader = SDL_CreateGPUShader(device, &vertexInfo);

    // free the file
    SDL_free(vertexCode);

    // Fragment Shader
    // ---------------

    // load the fragment shader code
    size_t fragmentCodeSize;
    // compile the shader in commant prompt:
    // glslc -fshader-stage=fragment shaders/fragment.glsl -o shaders/fragment.spv
    void* fragmentCode = SDL_LoadFile("shaders/fragment.spv", &fragmentCodeSize);

    // create the fragment shader
    SDL_GPUShaderCreateInfo fragmentInfo{};
    fragmentInfo.code = (Uint8*)fragmentCode;
    fragmentInfo.code_size = fragmentCodeSize;
    fragmentInfo.entrypoint = "main";
    fragmentInfo.format = SDL_GPU_SHADERFORMAT_SPIRV;
    fragmentInfo.stage = SDL_GPU_SHADERSTAGE_FRAGMENT;
    fragmentInfo.num_samplers = 0;
    fragmentInfo.num_storage_buffers = 0;
    fragmentInfo.num_storage_textures = 0;
    fragmentInfo.num_uniform_buffers = 1; // update fragment shader - Now we use a uniform buffer

    SDL_GPUShader* fragmentShader = SDL_CreateGPUShader(device, &fragmentInfo);

    // free the file
    SDL_free(fragmentCode);

    // Create Graphics Pipeline
    // ------------------------
    
    // A pipeline describes:
    //    shaders
    //    vertex layout
    //    blending
    //    primitive type

    SDL_GPUGraphicsPipelineCreateInfo pipelineInfo{};
    pipelineInfo.vertex_shader = vertexShader;
    pipelineInfo.fragment_shader = fragmentShader;
    pipelineInfo.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;

    // describe the vertex buffers
    SDL_GPUVertexBufferDescription vertexBufferDesctiptions[1];
    vertexBufferDesctiptions[0].slot = 0;
    vertexBufferDesctiptions[0].input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX;
    vertexBufferDesctiptions[0].instance_step_rate = 0;
    vertexBufferDesctiptions[0].pitch = sizeof(Vertex);

    pipelineInfo.vertex_input_state.num_vertex_buffers = 1;
    pipelineInfo.vertex_input_state.vertex_buffer_descriptions = vertexBufferDesctiptions;

    // describe the vertex attribute
    
    SDL_GPUVertexAttribute vertexAttributes[2];

    // a_position // Position attribute (vec3)
    vertexAttributes[0].buffer_slot = 0;
    vertexAttributes[0].location = 0;
    vertexAttributes[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3;
    vertexAttributes[0].offset = 0;

    // a_color // Color attribute (vec4)
    vertexAttributes[1].buffer_slot = 0;
    vertexAttributes[1].location = 1;
    vertexAttributes[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4;
    vertexAttributes[1].offset = sizeof(float) * 3;

    pipelineInfo.vertex_input_state.num_vertex_attributes = 2;
    pipelineInfo.vertex_input_state.vertex_attributes = vertexAttributes;

    // Color Target (swapchain)
    // ------------------------
    
    // describe the color target
    SDL_GPUColorTargetDescription colorTargetDescriptions[1];
    colorTargetDescriptions[0] = {};
    colorTargetDescriptions[0].blend_state.enable_blend = true;
    colorTargetDescriptions[0].blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD;
    colorTargetDescriptions[0].blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD;
    colorTargetDescriptions[0].blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA;
    colorTargetDescriptions[0].blend_state.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
    colorTargetDescriptions[0].blend_state.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA;
    colorTargetDescriptions[0].blend_state.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
    colorTargetDescriptions[0].format = SDL_GetGPUSwapchainTextureFormat(device, window);

    pipelineInfo.target_info.num_color_targets = 1;
    pipelineInfo.target_info.color_target_descriptions = colorTargetDescriptions;

    // create the pipeline
    graphicsPipeline = SDL_CreateGPUGraphicsPipeline(device, &pipelineInfo);

    // we don't need to store the shaders after creating the pipeline
    SDL_ReleaseGPUShader(device, vertexShader);
    SDL_ReleaseGPUShader(device, fragmentShader);

    // Create Vertex Buffer & Upload Data
    // ----------------------------------
    
    // Create GPU Buffer
    SDL_GPUBufferCreateInfo bufferInfo{};
    bufferInfo.size = sizeof(vertices);
    bufferInfo.usage = SDL_GPU_BUFFERUSAGE_VERTEX;
    vertexBuffer = SDL_CreateGPUBuffer(device, &bufferInfo);

    // create a transfer buffer (staging buffer) to upload to the vertex buffer
    SDL_GPUTransferBufferCreateInfo transferInfo{};
    transferInfo.size = sizeof(vertices);
    transferInfo.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
    transferBuffer = SDL_CreateGPUTransferBuffer(device, &transferInfo);

    // fill the transfer buffer
    Vertex* data = (Vertex*)SDL_MapGPUTransferBuffer(device, transferBuffer, false);

    SDL_memcpy(data, (void*)vertices, sizeof(vertices));

    // data[0] = vertices[0];
    // data[1] = vertices[1];
    // data[2] = vertices[2];

    SDL_UnmapGPUTransferBuffer(device, transferBuffer);

    // start a copy pass // Copy Transfer Buffer → Vertex Buffer
    SDL_GPUCommandBuffer* commandBuffer = SDL_AcquireGPUCommandBuffer(device);
    SDL_GPUCopyPass* copyPass = SDL_BeginGPUCopyPass(commandBuffer);

    // where is the data
    SDL_GPUTransferBufferLocation location{};
    location.transfer_buffer = transferBuffer;
    location.offset = 0;

    // where to upload the data
    SDL_GPUBufferRegion region{};
    region.buffer = vertexBuffer;
    region.size = sizeof(vertices);
    region.offset = 0;

    // upload the data
    SDL_UploadToGPUBuffer(copyPass, &location, &region, true);

    // end the copy pass
    SDL_EndGPUCopyPass(copyPass);
    SDL_SubmitGPUCommandBuffer(commandBuffer);

    return SDL_APP_CONTINUE;
}

// SDL_AppIterate — Per‑Frame Rendering
// ------------------------------------

SDL_AppResult SDL_AppIterate(void* appstate)
{
    // acquire the command buffer
    SDL_GPUCommandBuffer* commandBuffer = SDL_AcquireGPUCommandBuffer(device);

    // get the swapchain texture
    SDL_GPUTexture* swapchainTexture;
    Uint32 width, height;
    SDL_WaitAndAcquireGPUSwapchainTexture(commandBuffer, window, &swapchainTexture, &width, &height);

    // end the frame early if a swapchain texture is not available
    if (swapchainTexture == NULL)
    {
        // you must always submit the command buffer
        SDL_SubmitGPUCommandBuffer(commandBuffer);
        return SDL_APP_CONTINUE;
    }

    // create the color target
    SDL_GPUColorTargetInfo colorTargetInfo{};
    colorTargetInfo.clear_color = { 30 / 255.0f, 200 / 255.0f, 120 / 255.0f, 255 / 255.0f };
    colorTargetInfo.load_op = SDL_GPU_LOADOP_CLEAR;
    colorTargetInfo.store_op = SDL_GPU_STOREOP_STORE;
    colorTargetInfo.texture = swapchainTexture;

    // begin a render pass
    SDL_GPURenderPass* renderPass = SDL_BeginGPURenderPass(commandBuffer, &colorTargetInfo, 1, NULL);

    // draw calls start here // Issue Draw Call
    // -------------------------------------

    // bind the pipeline
    SDL_BindGPUGraphicsPipeline(renderPass, graphicsPipeline);

    // bind the vertex buffer
    SDL_GPUBufferBinding bufferBindings[1];
    bufferBindings[0].buffer = vertexBuffer;
    bufferBindings[0].offset = 0;

    SDL_BindGPUVertexBuffers(renderPass, 0, bufferBindings, 1);


    // Push the uniform to the fragment shader, and directly send to the GPU right before the draw call
    // Uniforms is small and fast.
    timeUniform.time = SDL_GetTicksNS() / 1e9f; // the time since the app started in seconds
    SDL_PushGPUFragmentUniformData(commandBuffer, 0, &timeUniform, sizeof(UniformBuffer));

    // issue a draw call // Draw 3 vertices
    SDL_DrawGPUPrimitives(renderPass, 3, 1, 0, 0);

    // ------------------------------------

    // Finish Frame
    // ------------
    
    // end the render pass
    SDL_EndGPURenderPass(renderPass);

    // submit the command buffer
    SDL_SubmitGPUCommandBuffer(commandBuffer);

    return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event)
{
    // close the window on request
    if (event->type == SDL_EVENT_WINDOW_CLOSE_REQUESTED)
    {
        return SDL_APP_SUCCESS;
    }

    return SDL_APP_CONTINUE;
}

// SDL_AppQuit — Cleanup
// ---------------------

void SDL_AppQuit(void* appstate, SDL_AppResult result)
{
    // release buffers
    SDL_ReleaseGPUBuffer(device, vertexBuffer);
    SDL_ReleaseGPUTransferBuffer(device, transferBuffer);

    // release the pipeline
    SDL_ReleaseGPUGraphicsPipeline(device, graphicsPipeline);

    // destroy the GPU device
    SDL_DestroyGPUDevice(device);

    // destroy the window
    SDL_DestroyWindow(window);
}

/*
1. Vertex Data Layout (CPU‑side)
This shows how your Vertex struct is interpreted by the GPU.

Explanation
Each vertex contains position (vec3) and color (vec4).

The GPU reads this memory using the vertex buffer description and vertex attributes you defined.

The offsets match your shader’s layout(location = 0) and layout(location = 1) inputs.

2. GPU Pipeline Structure
This shows the major components of the graphics pipeline SDL_GPU builds.

Key Stages
Vertex Shader
Transforms each vertex and passes color to the fragment shader.

Primitive Assembly
Groups vertices into triangles (you used TRIANGLELIST).

Rasterizer
Converts triangles into fragments (pixels).

Fragment Shader
Computes final pixel color using interpolated vertex colors.

Output Merger
Applies blending and writes to the swapchain texture.

3. Command Buffer & Render Pass Flow
This shows how SDL_GPU organizes rendering commands.

Flow
Acquire command buffer

Acquire swapchain texture

Begin render pass

Bind pipeline

Bind vertex buffer

Issue draw call

End render pass

Submit command buffer

SDL_GPU uses a modern API design similar to Vulkan/Metal/DX12.


4. Final Frame Rendering Path
This shows how the triangle ends up on the screen.

Steps
Vertex buffer → vertex shader

Triangle rasterized

Fragment shader colors pixels

Render pass writes to swapchain texture

Swapchain presents to the window

*/

 

// vertex.glsl
// OpenGL 4.6 → Vulkan SPIR‑V compatible vertex shader
//
// Notes:
// - Vulkan requires explicit 'location' qualifiers for all inputs/outputs.
// - No default locations are assumed, unlike classic OpenGL.
// - This shader simply passes position to gl_Position and forwards color
//   to the fragment shader.

#version 460

// -----------------------------------------------------------------------------
// VERTEX INPUT ATTRIBUTES (from your vertex buffer)
// -----------------------------------------------------------------------------
// a_position: vec3 at location 0
//   - The vertex position in object space.
//   - Vulkan will feed this from a bound vertex buffer.
//
// a_color: vec4 at location 1
//   - Per‑vertex color.
//   - Interpolated across the triangle and passed to the fragment shader.
layout (location = 0) in vec3 a_position;
layout (location = 1) in vec4 a_color;

// -----------------------------------------------------------------------------
// OUTPUT TO FRAGMENT SHADER
// -----------------------------------------------------------------------------
// v_color: vec4 at location 0
//   - This value is interpolated across the primitive and received by the
//     fragment shader at the same location index.
layout (location = 0) out vec4 v_color;

// -----------------------------------------------------------------------------
// MAIN VERTEX PROCESSING
// -----------------------------------------------------------------------------
void main()
{
    // Convert the incoming vec3 position into a vec4 clip‑space position.
    // The w‑component must be 1.0 for proper homogeneous coordinates.
    gl_Position = vec4(a_position, 1.0f);

    // Pass the vertex color to the fragment shader.
    // Interpolation happens automatically.
    v_color = a_color;
}

 

// fragment.glsl
// OpenGL 4.6 → Vulkan SPIR‑V compatible fragment shader
//
// Recompile using glslc (Vulkan SDK):
// glslc -fshader-stage=fragment shaders/fragment.glsl -o shaders/fragment.spv
//
// Notes:
// - Vulkan requires explicit layout qualifiers (set, binding, location).
// - std140 layout is used for uniform blocks to match Vulkan buffer rules.

#version 460

// -----------------------------------------------------------------------------
// INPUTS FROM VERTEX SHADER
// -----------------------------------------------------------------------------
// 'v_color' comes from the vertex shader at location 0.
// It carries the interpolated per‑vertex color into the fragment stage.
layout (location = 0) in vec4 v_color;

// -----------------------------------------------------------------------------
// FRAGMENT OUTPUT
// -----------------------------------------------------------------------------
// 'FragColor' is the final color written to the framebuffer.
// Vulkan also uses explicit location qualifiers for outputs.
layout (location = 0) out vec4 FragColor;

// -----------------------------------------------------------------------------
// UNIFORM BLOCK (VULKAN STYLE)
// -----------------------------------------------------------------------------
// Vulkan requires descriptor set + binding numbers.
// std140 ensures predictable memory layout for uniform buffers.
//
// set = 3, binding = 0 → this must match your descriptor layout in Vulkan.
// The block contains a single float: 'time', updated each frame.
layout(std140, set = 3, binding = 0) uniform UniformBlock {
    float time;
};

// -----------------------------------------------------------------------------
// MAIN FRAGMENT LOGIC
// -----------------------------------------------------------------------------
void main()
{
    // Create a pulsing value based on time.
    // sin(time * 3.0) oscillates between -1 and 1.
    // Multiply by 0.5 → range becomes [-0.5, 0.5].
    // Add 0.5 → final range becomes [0, 1].
    float pulse = sin(time * 3.0) * 0.5 + 0.5;

    // Apply the pulse to the incoming color.
    // Base brightness is 0.7, pulse adds up to +0.5.
    // Alpha is preserved from the vertex color.
    FragColor = vec4(v_color.rgb * (0.7 + pulse * 0.5), v_color.a);
}

 



answered Jul 24 by avibootz
edited Jul 24 by avibootz
...