#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, ®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);
// 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);
}