#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)
};
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 = 0;
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, ®ion, 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);
// 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 - converting to Vulkan SPIRV
#version 460
layout (location = 0) in vec3 a_position;
layout (location = 1) in vec4 a_color;
layout (location = 0) out vec4 v_color;
void main()
{
gl_Position = vec4(a_position, 1.0f);
v_color = a_color;
}
// fragment.glsl
// OpenGL 4.6 - converting to Vulkan SPIRV
#version 460
layout (location = 0) in vec4 v_color;
layout (location = 0) out vec4 FragColor;
void main()
{
FragColor = v_color;
}