Sitemap
Timur Gafarov

Timur Gafarov

Computer graphics developer

Implementing a Renderer in SDL GPU, part 1

4 min readMay 9, 2026
Press enter or click to view image in full size

I’ve been doing graphics in OpenGL for many years, since 2009, and I believe this classic API is still the most obvious choice if you need to render something in real time in a cross-platform way. Combined with SDL, it’s also incredibly simple — the application code, with all its key components (window, graphics, user input), is 99% independent of the operating system. For 2D projects, this combination is more than sufficient, but for games with advanced 3D graphics, pushing the GPU to the limits, things are unfortunately more complicated now.

I identify three main architectural problems with OpenGL: no thread-safety, frequent CPU/GPU synchronization, and the complexity of managing global pipeline state. New low-level APIs address all of these, especially the last one, at the expense of development simplicity and the overall entry barrier to becoming a graphics developer. For a long time, there was an insurmountable wall of conceptual incompatibility between OpenGL and Vulkan. Meanwhile, OpenGL, while still around, has stagnated. Under macOS, it’s an old version, and under Windows, there’s a fairly serious issue with vertical sync in windowed mode, leading to stuttering (this can only be fully resolved by running over the DXGI swapchain, which requires a non-trivial boilerplate in the application or support from the video driver). And on top of that, OpenGL lacks support for Windows’ HDR mode, which isn’t critical, but it’s not great.

All this leads me to the conclusion that OpenGL, no matter how convenient it is, has already outlived its usefulness. But I also have absolutely no desire to switch to Vulkan. After getting acquainted with SDL GPU, I decided to try porting some key parts of my engine to it and quickly realized that this API is exactly what I’ve been missing for the last few years (WebGPU honestly was a disappointment due to its cumbersome interfaces and unstable API).

The main appeal of SDL GPU is its minimalism. Initialization structures don’t look like monstrous JavaScript objects. Binding a renderer to a window is a single SDL_ClaimWindowForGPUDevice call, in contrast to a saga of platform-specific boilerplate in WebGPU (this is because SDL completely handles window management by itself). There’s also no queue management and almost no need to worry about synchronization in simple use cases: you simply create command buffers for your tasks and submit them for execution using the SDL_SubmitGPUCommandBuffer function. A command buffer, as in other low-level APIs, is a list of commands to the GPU to perform an operation (create a resource, copy data, draw a primitive).

Data is transferred to video memory using so-called transfer buffers (SDL_GPUTransferBuffer). These are mapped to system memory, so you can copy data into them using memcpy or any other convenient method. Once a transfer buffer is ready, it is used as a source for the copy pass (SDL_GPUCopyPass), which is in turn written to the command buffer. Overall, the core idea of ​​the explicit APIs (reducing the CPU and GPU synchronization frequency to the necessary minimum) has been carefully preserved, and it shows.

Get Timur Gafarov’s stories in your inbox

Join Medium for free to get updates from this writer.

A particular headache for WebGPU is swapchain management, and SDL3 GPU eliminates all the complexity here as well. You simply get the current back buffer with SDL_WaitAndAcquireGPUSwapchainTexture, update your color target, and use it to call SDL_BeginGPURenderPass, and that’s it!

There’s no single shader language in SDL GPU, it depends on the chosen backend. If you’ve chosen Vulkan, it’s SPIR-V; if Direct3D 12, then it’s DXIL; if Metal, it’s MSL. So, for a multi-target engine, you’ll definitely need a shader translation toolchain, such as SPIRV-Cross. This is probably the only difficulty, especially if you don’t use C/C++. But for D, I’ve already solved this problem by writing bindbc-glslang and bindbc-spirvcross bindings. Manually compiling shaders may seem a bit tricky, but in reality, it’s a “set it and forget it” process. Using SPIR-V as an internal intermediate representation gives you unprecedented freedom — you can write in the familiar GLSL, or you can try something new. There is also a SDL_shadercross helper library that hides all the nitty-gritty of shader compilation under a convenient API.

Binding resources to shaders is a whole other story. SDL has solved this problem elegantly, with all four descriptor sets strictly assigned:
- Set 0 — vertex-stage textures/samplers and SSBOs
- Set 1 — vertex-stage UBOs
- Set 2 — fragment-stage textures/samplers and SSBOs
- Set 3 — fragment-stage UBOs

You define the binding points yourself (in the shader and in the code). The only important thing to know before creating a shader is how many samplers, SSBOs, and UBOs it uses. This can be confusing, as it complicates the abstraction in the engine, but here’s a lifehack: this information is provided by SPIRV-Cross (see spvc_resources_get_resource_list_for_type). I think it’s a godsend compared to WebGPU.

SDL GPU-based renderer uses the SDL_GPUGraphicsPipeline objects (which define rasterizer parameters) and the SDL_GPURenderPass objects (to which resources are bound for drawing). In a deferred renderer, these objects can be conveniently combined into a single RenderPass abstraction. Pipelines are traditionally immutable, so after OpenGL, you’ll have to get used to the new reality and optimize rendering.

That’s about it, I think. I haven’t touched on compute yet; that’s a topic for a separate article series. SDL developers have pleasantly surprised me by rolling out an excellent API in a relatively short time — it’s already quite capable of handling serious projects. I hope it continues to evolve and thrive.

In the next part of this series, I’ll go into more detail about the science behind the rendering techniques that I implemented.

--

--