Skip to content

Instantly share code, notes, and snippets.

@API-Beast
Last active January 29, 2026 15:16
Show Gist options
  • Select an option

  • Save API-Beast/5dcaac8271bc51183be2e4ccee00a2f5 to your computer and use it in GitHub Desktop.

Select an option

Save API-Beast/5dcaac8271bc51183be2e4ccee00a2f5 to your computer and use it in GitHub Desktop.
SDL GPU with SDL Renderer
#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL_main.h>
#include <SDL3/SDL.h>
#include "common/viewport.h"
#include <math.h>
#define CONFIG_LIST \
HEADER("Shader Storage Buffer")\
FLOAT_SLIDER(hue_shift, 0.0f, -1.0f, 1.0f, 0.01f, 1.0f)
#include "common/config_ui.h"
config_data config;
SDL_Window* window;
SDL_Renderer* renderer;
SDL_GPUDevice* device;
SDL_GPUGraphicsPipeline* pipeline;
const int vertex_buffer_max = 1024;
const int storage_buffer_max = 1024;
SDL_GPUBuffer* vertex_buffer;
SDL_GPUBuffer* storage_buffer;
SDL_GPUTransferBuffer* upload_buffer = NULL;
SDL_Texture* render_target;
SDL_GPUTexture* render_target_gpu_view;
struct Vertex
{
float X, Y;
float U, V;
};
int window_width = 800;
int window_height = 400;
viewport map_view = (viewport){
.offset = {-200.0f, -200.0f},
.scale = 1.0f,
.position = {0.0f, 0.0f},
.size = {600.0f, 400.0f},
.started_drag = false,
.x_dir = {1.0f, 0.0f},
.y_dir = {0.0f, 1.0f}
};
viewport config_view = (viewport){
.offset = {0.0f, 0.0f},
.scale = 1.0f,
.position = {600.0f, 0.0f},
.size = {200.0f, 400.0f},
.started_drag = false,
.x_dir = {0.0f, 0.0f},
.y_dir = {0.0f, 1.0f}
};
const unsigned char vertex_shader_spv[] = {
#embed "C:/Users/Beast/Desktop/website/build/src/gpurenderer/shaders/vertex.spv"
};
const unsigned char fragment_shader_spv[] = {
#embed "C:/Users/Beast/Desktop/website/build/src/gpurenderer/shaders/fragment.spv"
};
void update_gpu_memory()
{
if(!upload_buffer)
upload_buffer = SDL_CreateGPUTransferBuffer(device, &(SDL_GPUTransferBufferCreateInfo){.size = vertex_buffer_max + storage_buffer_max, .usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD});
void* target = SDL_MapGPUTransferBuffer(device, upload_buffer, true);
vec2f tl = map_view.position + (vec2f){10., 10.};
vec2f br = map_view.position + map_view.size - (vec2f){10., 10.};
struct Vertex vertices[] = {
{tl[0], tl[1], 0.0f, 0.0f}, // 0: top-left
{tl[0], br[1], 0.0f, 1.0f}, // 1: bottom-left
{br[0], tl[1], 1.0f, 0.0f}, // 2: top-right
{br[0], br[1], 1.0f, 1.0f} // 3: bottom-right
};
SDL_memcpy(target, vertices, sizeof(vertices));
int vertex_buffer_length = sizeof(vertices);
float pixel_to_clip[] = {
2.0f / window_width, 0.0f, 0.0f, 0.0f,
0.0f, -2.0f / window_height, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f, 1.0f
};
SDL_memcpy(target+vertex_buffer_max, pixel_to_clip, sizeof(pixel_to_clip));
SDL_memcpy(target+vertex_buffer_max+sizeof(pixel_to_clip), &config.hue_shift, sizeof(float));
int storage_buffer_length = sizeof(pixel_to_clip) + sizeof(float);
SDL_UnmapGPUTransferBuffer(device, upload_buffer);
SDL_GPUCommandBuffer* commandBuffer = SDL_AcquireGPUCommandBuffer(device);
SDL_GPUCopyPass* copyPass = SDL_BeginGPUCopyPass(commandBuffer);
SDL_UploadToGPUBuffer(copyPass,
&(SDL_GPUTransferBufferLocation){.transfer_buffer=upload_buffer, .offset = 0},
&(SDL_GPUBufferRegion){.buffer=vertex_buffer, .offset = 0, .size = vertex_buffer_length}, true);
SDL_UploadToGPUBuffer(copyPass,
&(SDL_GPUTransferBufferLocation){.transfer_buffer=upload_buffer, .offset = vertex_buffer_max},
&(SDL_GPUBufferRegion){.buffer=storage_buffer, .offset = 0, .size = storage_buffer_length}, true);
SDL_EndGPUCopyPass(copyPass);
SDL_SubmitGPUCommandBuffer(commandBuffer);
}
SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[])
{
SDL_Init(SDL_INIT_VIDEO);
window = SDL_CreateWindow("Demo", window_width, window_height, 0);
device = SDL_CreateGPUDevice(SDL_GPU_SHADERFORMAT_SPIRV, true, NULL);
// Note, SDL_CreateGPURenderer will claim the window. We aren't allowed to do this ourselves or we will get Vulkan errors.
renderer = SDL_CreateGPURenderer(device, window);
if(device == NULL || renderer == NULL || window == NULL)
{
const char* error = SDL_GetError();
printf("SDL Initialization failed (%x %x %x)\nReason: ", device, renderer, window);
if(error != NULL){ puts(error); }
return SDL_APP_FAILURE;
}
SDL_GPUGraphicsPipelineCreateInfo pipe_info = {
.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLESTRIP,
.vertex_input_state =
{
.num_vertex_buffers = 1,
.vertex_buffer_descriptions = (SDL_GPUVertexBufferDescription[]){
[0] = {.slot = 0, .input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX, .instance_step_rate = 0, .pitch = sizeof(struct Vertex)}
},
.num_vertex_attributes = 2,
.vertex_attributes = (SDL_GPUVertexAttribute[]){
[0] = {.buffer_slot = 0, .location = 0, .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2, .offset = 0},
[1] = {.buffer_slot = 0, .location = 1, .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2, .offset = offsetof(struct Vertex, U)}
}
},
.target_info =
{
.num_color_targets = 1,
.color_target_descriptions = (SDL_GPUColorTargetDescription[])
{
[0] = {.format = SDL_GetGPUSwapchainTextureFormat(device, window)}
}
}
};
SDL_GPUShaderCreateInfo vert_info = {
.entrypoint = "main", .format = SDL_GPU_SHADERFORMAT_SPIRV, .stage = SDL_GPU_SHADERSTAGE_VERTEX,
.code = vertex_shader_spv, .code_size = sizeof(vertex_shader_spv),
.num_storage_buffers = 1
};
SDL_GPUShaderCreateInfo frag_info = {
.entrypoint = "main", .format = SDL_GPU_SHADERFORMAT_SPIRV, .stage = SDL_GPU_SHADERSTAGE_FRAGMENT,
.code = fragment_shader_spv, .code_size = sizeof(fragment_shader_spv),
.num_storage_buffers = 1
};
pipe_info.vertex_shader = SDL_CreateGPUShader(device, &vert_info);
pipe_info.fragment_shader = SDL_CreateGPUShader(device, &frag_info);
pipeline = SDL_CreateGPUGraphicsPipeline(device, &pipe_info);
SDL_ReleaseGPUShader(device, pipe_info.vertex_shader);
SDL_ReleaseGPUShader(device, pipe_info.fragment_shader);
vertex_buffer = SDL_CreateGPUBuffer(device, &(SDL_GPUBufferCreateInfo){.size = vertex_buffer_max, .usage = SDL_GPU_BUFFERUSAGE_VERTEX});
storage_buffer = SDL_CreateGPUBuffer(device, &(SDL_GPUBufferCreateInfo){.size = storage_buffer_max, .usage = SDL_GPU_BUFFERUSAGE_GRAPHICS_STORAGE_READ});
// Note: Specifically Renderer textures. We can't turn GPUTextures into Renderer Textures.
SDL_PixelFormat format = SDL_GetPixelFormatFromGPUTextureFormat(SDL_GetGPUSwapchainTextureFormat(device, window));
int w, h;
SDL_GetRenderOutputSize(renderer, &w, &h);
render_target = SDL_CreateTexture(renderer, format, SDL_TEXTUREACCESS_TARGET, w, h);
render_target_gpu_view = SDL_GetPointerProperty(SDL_GetTextureProperties(render_target), SDL_PROP_TEXTURE_GPU_TEXTURE_POINTER, NULL);
update_gpu_memory();
config_init(&config);
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppIterate(void* appstate) {
SDL_FlushRenderer(renderer);
// Do SDL GPU stuff.
SDL_GPUCommandBuffer* commandBuffer = SDL_AcquireGPUCommandBuffer(device);
SDL_GPUTexture* swapchainTexture;
SDL_GPUColorTargetInfo colorTargetInfo = {.load_op = SDL_GPU_LOADOP_CLEAR, .store_op = SDL_GPU_STOREOP_STORE, .texture = render_target_gpu_view};
SDL_GPURenderPass* renderPass = SDL_BeginGPURenderPass(commandBuffer, &colorTargetInfo, 1, NULL);
SDL_BindGPUGraphicsPipeline(renderPass, pipeline);
SDL_BindGPUVertexStorageBuffers(renderPass, 0, &storage_buffer, 1);
SDL_BindGPUFragmentStorageBuffers(renderPass, 0, &storage_buffer, 1);
SDL_BindGPUVertexBuffers(renderPass, 0, &(SDL_GPUBufferBinding){.buffer = vertex_buffer, .offset = 0}, 1);
SDL_DrawGPUPrimitives(renderPass, 4, 1, 0, 0);
SDL_EndGPURenderPass(renderPass);
SDL_SubmitGPUCommandBuffer(commandBuffer);
// Blit the stuff we did in SDL GPU onto the actual swapchain texture.
SDL_RenderTexture(renderer, render_target, NULL, NULL);
// Now we can let Renderer handle the rest of the drawing.
config_render_ui(renderer, &config, config_view.position[0], config_view.position[1], config_view.size[0], config_view.size[1]);
SDL_RenderPresent(renderer);
if(config.any_changed)
update_gpu_memory();
config_reset_modified(&config);
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event)
{
if (event->type == SDL_EVENT_QUIT)
{
return SDL_APP_SUCCESS;
}
if(config_handle_input(event, &config, config_view.position[0], config_view.position[1], config_view.size[0], config_view.size[1]))
{
return SDL_APP_CONTINUE;
}
return SDL_APP_CONTINUE;
}
void SDL_AppQuit(void* appstate, SDL_AppResult result)
{
// These are really just because the Vulkan validation layer complains otherwise.
if (storage_buffer) SDL_ReleaseGPUBuffer(device, storage_buffer);
if (vertex_buffer) SDL_ReleaseGPUBuffer(device, vertex_buffer);
if (upload_buffer) SDL_ReleaseGPUTransferBuffer(device, upload_buffer);
if (render_target) SDL_DestroyTexture(render_target);
if (pipeline) SDL_ReleaseGPUGraphicsPipeline(device, pipeline);
// The actual things to deinit.
if (renderer) SDL_DestroyRenderer(renderer);
if (device) SDL_DestroyGPUDevice(device);
if (window) SDL_DestroyWindow(window);
SDL_Quit();
}
#version 460
layout(set = 2, binding = 0) readonly buffer StorageBuffer {
mat4 view;
float hue_shift;
} storage;
layout(location = 0) in vec2 uv;
layout(location = 0) out vec4 frag_colour;
vec3 hueShift(vec3 color, float hueShift) {
vec3 k = vec3(0.57735, 0.57735, 0.57735); // 1/sqrt(3)
float cosAngle = cos(hueShift * 6.283185); // 2*PI
return color * cosAngle + cross(k, color) * sin(hueShift * 6.283185) + k * dot(k, color) * (1.0 - cosAngle);
}
void main()
{
frag_colour = vec4(hueShift(vec3(uv * 0.5, 1.0), storage.hue_shift), 1.0);
}
#version 460
layout(set = 0, binding = 0) readonly buffer StorageBuffer {
mat4 view;
float hue_shift;
} storage;
layout (location = 0) in vec2 a_position;
layout (location = 1) in vec2 uv;
layout (location = 0) out vec2 uv_out;
void main()
{
gl_Position = storage.view * vec4(a_position, 1.0f, 1.0f);
uv_out = uv;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment