Now that we've gotten the "make sure we can compile, link, and run an SDL program" step out of the way, we can start actually doing some work. We'll start by going over Window creation and setting up your event loop so we can receive events, like the one that tells you the program should end.
The first thing we'll need to clear the window is the window. So let's look into how to get an SDL_Window. Thankfully SDL makes it pretty easy, we just need the title we'd like, the width and height, and some flags. We'll go with the name of this chapter, and 720p. We can ignore the flags for now, for the most part they're not relevant to SDL_GPU.
SDL_Window* window = SDL_CreateWindow("002_Window_and_Clearing", 1280, 720, 0); if (!window) { SDL_Log("Couldn't initialize SDL: %s", SDL_GetError()); return 1; } // This is where the Event loop we cover in the next section will go! SDL_DestroyWindow(window);
So we've created a simple window, checked that it's initialized, and then destroyed it. I wouldn't advise you to run this, as how something like this gets displayed is platform dependant, but we can expect either a flash of a window, or nothing at all.
Something very important with any library, but particularly with SDL is error checking. SDL thankfully provides a macro for us to use, so lets adjust our code above. Feel free to use whatever is most ergonomic for you or your language of choice if you're not using C, or would like more detailed errors.
SDL_assert(SDL_Init(SDL_INIT_VIDEO)); SDL_Window* window = SDL_CreateWindow("002-Window_and_Clearing", 1280, 720, 0); SDL_assert(window);
To briefly explain, for most Game-like applications, you're going to use a couple top-level loops:
The specific formulations of the above vary, but we'll just being using the typical formulation you'd find in most beginner games:
bool running = true; // Outer "Frame" Loop while (running) { SDL_Event event; // Event Loop, must exhaust all events every frame. while (SDL_PollEvent(&event)) { switch (event.common.type) { case SDL_EVENT_QUIT: running = false; break; } } // This is where you'd do most of your per-frame work. Gameplay, Physics, Rendering. // This is where almost all the code we'll be writing below is going, besides the functions of course! }
So as mentioned, we have a bool that we can modify to exit the frame loop, and within that we have an Event loop. We loop over SDL_PollEvent, which returns true whenever it finds a new event. Right now all we need to deal with are SDL_EVENT_QUIT events, which we'll use to set running to false to end the application. It generally comes when the last Window open is closed.
Now, finally, we can discuss the GPU API.
SDL_CreateWindow
SDL_CreateWindowWithProperties
SDL_DestroyWindow
SDL_CreateWindow, it just brings the Window and related resources down.The Events Subsystem
SDL_PollEvent
SDL_PollEvent or one of the related functions.SDL_Event, SDL_CommonEvent, SDL_EVENT_QUIT of SDL_EventType, and it's struct SDL_QuitEvent
SDL_Event is a union of structs SDL uses to pass us every event that it wants to inform us about. It contains within it every event struct, and we can differentiate between them by examining the type field within the SDL_CommonEvent and checking it against the enums from SDL_EventType. In this case, we just wanted to know when the user was requesting us to quit, which is fired when the user asks to close the last window as one example.SDL_Event also directly contains a type field you can inspect to know the type. That said, for incredibly boring and technical details, in C++ specifically using this field to know which union member to use is undefined behavior. So I avoid doing so, even though in practice, every compiler I'm aware of treats this case as-if it were C and thus works as expected.
Finally, we can do one of the first things you'll ever do in a Graphics API, create a Device and claim the Window.
SDL_GPUDevice* device = SDL_CreateGPUDevice(SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_DXIL | SDL_GPU_SHADERFORMAT_MSL, true, "vulkan"); SDL_assert(device); SDL_assert(SDL_ClaimWindowForGPUDevice(mDevice, mWindow));
Pretty easy right? If you've dabbled in some of the APIs SDL_GPU is built on top of, you'd know initialization can be a bit of a bear at times. SDL_GPU generally takes care of most of those details, later on we'll look at how some of these details can be tweaked using a Properties version of SDL_CreateGPUDevice.
For now, we'll tell SDL_CreateGPUDevice that we can give SDL any of the backend shader formats. We'd like to create the Device in debug mode, so that things like the Vulkan Validation Layers, as well as SDL itself can do extra checking on your GPU operations, so we pass true to the debug parameter. Typically in a published build, you wouldn't ask for a Device to be created in Debug mode, but we're learning here, and more information is always better! Finally we'll pass in that we'd like to use the Vulkan backend.
If you're not getting a device created, it's likely that there's an issue with your drivers, your Vulkan SDK installation (on MacOS), or your GPU not supporting it. You can try to pass NULL as the final parameter to see if any of the backends are supported on your device, but note that this may cause issues or discrepancies when we cover debugging topics.
Now that we have a device, we can call SDL_ClaimWindowForGPUDevice to do what it says in the name: associate the GPUDevice and the Window. Just know that to render to our Window, we need to claim it for our Device. This is how, later on, we'll be able to retrieve swapchain textures (essentially the texture that the Window displays) and render to them.
Lets take a look at some initialization that we won't need just yet, but will help out in future examples. Fundamentally, we're just going to cache some checks on which formats SDL wanted shaders in, and create a properties object that we'll use to populate debug names when we start writing functions to create GPU resources.
While we're at it, let's make a context struct, and wrap all of this in a function.
typedef struct GpuContext { SDL_Window* mWindow; SDL_GPUDevice* mDevice; SDL_PropertiesID mProperties; const char* mShaderEntryPoint; SDL_GPUShaderFormat mChosenBackendFormat; } GpuContext; GpuContext CreateGpuContext(SDL_Window* aWindow) { GpuContext context; SDL_zero(context); context.mWindow = aWindow; context.mDevice = SDL_CreateGPUDevice(SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_DXIL | SDL_GPU_SHADERFORMAT_MSL, true, NULL); SDL_assert(context.mDevice); SDL_assert(SDL_ClaimWindowForGPUDevice(context.mDevice, context.mWindow)); context.mProperties = SDL_CreateProperties(); SDL_assert(context.mProperties); SDL_GPUShaderFormat availableFormats = SDL_GetGPUShaderFormats(context.mDevice); context.mShaderEntryPoint = NULL; if (availableFormats & SDL_GPU_SHADERFORMAT_SPIRV) { context.mChosenBackendFormat = SDL_GPU_SHADERFORMAT_SPIRV; context.mShaderEntryPoint = "main"; } else if (availableFormats & SDL_GPU_SHADERFORMAT_MSL) { context.mChosenBackendFormat = SDL_GPU_SHADERFORMAT_MSL; context.mShaderEntryPoint = "main0"; } else if (availableFormats & SDL_GPU_SHADERFORMAT_DXIL) { context.mChosenBackendFormat = SDL_GPU_SHADERFORMAT_DXIL; context.mShaderEntryPoint = "main"; } return context; } void DestroyGpuContext(GpuContext* aContext) { SDL_DestroyProperties(aContext->mProperties); SDL_DestroyGpuDevice(aContext->mDevice); SDL_DestroyWindow(aContext->mWindow); SDL_zero(*aContext); }
So now we have a GpuContext struct we can pass around, it holds all of the stuff relevant to the Device for the sake of creating resources, and tearing them and it down. There's certainly more functionality we can add here, but we can revisit it later if it's helpful.
In terms of the functionality that we just added, as mentioned above, we've created an SDL properties object. This is a way to tell SDL about extra functionality we want. Often this is backend/platform specific information, but sometimes it's simply for extended initialization, as more can be added as needed, and it won't break SDLs API guarantees.
SDL_CreateGPUDevice
main. That said, I'll try to ensure they always stick to one file! (Plus the shader files.)SDL_DestroyGpuDevice
SDL_zero
SDL_ClaimWindowForGPUDevice
SDL_GetGPUShaderFormats
SDL_CreateProperties
SDL_DestroyProperties
We're near the finish line here. It's time to learn a bit about Command Buffers and the Swapchain Textures:
SDL_GPUCommandBuffer* commandBuffer = SDL_AcquireGPUCommandBuffer(context.mDevice); if (!commandBuffer) { SDL_Log("AcquireGPUCommandBuffer failed: %s", SDL_GetError()); continue; } SDL_GPUTexture* swapchainTexture; if (!SDL_WaitAndAcquireGPUSwapchainTexture(commandBuffer, context.mWindow, &swapchainTexture, NULL, NULL)) { SDL_Log("WaitAndAcquireGPUSwapchainTexture failed: %s", SDL_GetError()); continue; }
Not too bad, but you'll notice we didn't use SDL_assert. These two operations are okay to fail, if they do, we'll just skip rendering this frame. That said, you're probably wondering what these are.
A command buffer is how we record commands to instruct the GPU what to do. This includes things like uploading data in a Copy Pass, executing generic work on the GPUs many cores in a Compute Pass, and of course executing graphics related work in a Render Pass. We'll get into more details as this series moves along, but the command buffer is how we'll be doing the actual communication with the GPU. SDL_GPU handles the management of these, which you'll appreciate coming from something like Vulkan.
Once you have a command buffer, we can request a Swapchain texture. As mentioned earlier, this is the texture that is tied to, and gets displayed on the Window. By default SDL_GPU allocates 3 of them, that said this can be changed, as well as how precisely we wait for them, and if we wait at all. We'll try to cover some of these at a later time, for now this is a fairly simple way to handle acquisitions and submissions. The extra parameters which we've passed NULL to are simply to acquire the width and height of the given texture. This will become useful later, but we don't need it for now.
Now we can finally finish out the chapter by doing one of the "simplest" graphics applications, clearing the screen. Or, more specifically, clearing the swapchain texture we acquired and then displaying that texture onto the screen.
This requires a RenderPass, which is how we instruct the GPU to run through the vertex pipeline into the shading pipeline, out to an image. We'll go over that in more details in subsequent chapters, but you can think of this as the Pass which does most of the actual graphics work. There's also Copy and Compute Passes we have access to in SDL_GPU, and those essentially do what they sound like, letting you copy to/from GPU memory, and doing general purpose computing work respectively.
So the configuration of a RenderPass ignoring for a moment the additional work we can do once we have one, is mostly about the final image(s) we're outputting to. These are more generally called Targets, as you can both have more than one, and there are several kinds. These are what we'll be drawing to in subsequent chapters.
To clear the swapchain texture though, we actually only need to Begin and End a RenderPass, as this alone will allow us to configure the swapchain texture as a target, tell the RenderPass to clear it to a particular color, and to store that clear to the texture at the end. Lets see what that looks like:
SDL_GPUColorTargetInfo colorTargetInfo; SDL_zero(colorTargetInfo); colorTargetInfo.texture = swapchainTexture; colorTargetInfo.load_op = SDL_GPU_LOADOP_CLEAR; colorTargetInfo.store_op = SDL_GPU_STOREOP_STORE; colorTargetInfo.clear_color.r = 0.2f; colorTargetInfo.clear_color.g= 0.2f; colorTargetInfo.clear_color.b = 0.85f; colorTargetInfo.clear_color.a = 1.0f; SDL_GPURenderPass* renderPass = SDL_BeginGPURenderPass( commandBuffer, &colorTargetInfo, 1, NULL ); SDL_EndGPURenderPass(renderPass); SDL_SubmitGPUCommandBuffer(commandBuffer);
We did it! You should be seeing a window with a blue background!

SDL_BeginGPURenderPass is the first time for us that an SDL GPU call requires a fair bit of configuration, but it won't be the last by a long shot. SDL_GPUColorTargetInfo is one of many create/info structs we'll be going over. As will become tradition, we zero it out to "default" the fields, but right now, we can concern ourselves with just the 4 fields we're setting here:
SDL_GPU_LOADOP_CLEAR.As you may notice from the parameters of SDL_BeginGPURenderPass as well as the discussion above, you can actually pass an array of Color Targets. We'll be going through some very simple fullscreen effects in the next chapter, but when we get further along and learn more about textures, we can play around with this functionality with more interesting fullscreen effects. Similarly we'll get to the SDL_GPUDepthStencilTargetInfo parameter later on when we start playing with 3D.
After that, it's really just about ending the render pass with SDL_EndGPURenderPass and submitting the command buffer with SDL_SubmitGPUCommandBuffer to the GPU so that our commands are run.
SDL_AcquireGPUCommandBuffer
SDL_SubmitGPUCommandBuffer
SDL_SubmitGPUCommandBufferAndAcquireFence, which lets you get a fence to wait on if you need a command buffer to complete before proceeding, such as when doing a read of GPU memory from the CPU side.SDL_WaitAndAcquireGPUSwapchainTexture
SDL_GPUColorTargetInfo
SDL_BeginGPURenderPass
SDL_EndGPURenderPass
And now to move on to rendering some actual geometry, and using some very simple shaders!
#include <SDL3/SDL.h> // This is for testing to ensure the code works in both C and C++, // this entire preprocessor block should just be the #include // in your own code. #ifndef __cplusplus #include <SDL3/SDL_main.h> #else namespace cpp_test { #endif ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Shared GPU Code ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// typedef struct GpuContext { SDL_Window* mWindow; SDL_GPUDevice* mDevice; SDL_PropertiesID mProperties; const char* mShaderEntryPoint; SDL_GPUShaderFormat mChosenBackendFormat; const char* mChosenBackendFormatExtension; } GpuContext; GpuContext gContext; void CreateGpuContext(SDL_Window* aWindow) { SDL_zero(gContext); gContext.mWindow = aWindow; gContext.mDevice = SDL_CreateGPUDevice(SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_DXIL | SDL_GPU_SHADERFORMAT_MSL, true, NULL); SDL_assert(gContext.mDevice); SDL_assert(SDL_ClaimWindowForGPUDevice(gContext.mDevice, gContext.mWindow)); gContext.mProperties = SDL_CreateProperties(); SDL_assert(gContext.mProperties); SDL_GPUShaderFormat availableFormats = SDL_GetGPUShaderFormats(gContext.mDevice); gContext.mShaderEntryPoint = NULL; if (availableFormats & SDL_GPU_SHADERFORMAT_SPIRV) { gContext.mChosenBackendFormat = SDL_GPU_SHADERFORMAT_SPIRV; gContext.mShaderEntryPoint = "main"; gContext.mChosenBackendFormatExtension = "spv"; } else if (availableFormats & SDL_GPU_SHADERFORMAT_MSL) { gContext.mChosenBackendFormat = SDL_GPU_SHADERFORMAT_MSL; gContext.mShaderEntryPoint = "main0"; gContext.mChosenBackendFormatExtension = "msl"; } else if (availableFormats & SDL_GPU_SHADERFORMAT_DXIL) { gContext.mChosenBackendFormat = SDL_GPU_SHADERFORMAT_DXIL; gContext.mShaderEntryPoint = "main"; gContext.mChosenBackendFormatExtension = "dxil"; } } void DestroyGpuContext() { SDL_DestroyProperties(gContext.mProperties); SDL_DestroyGPUDevice(gContext.mDevice); SDL_DestroyWindow(gContext.mWindow); SDL_zero(gContext); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Main ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// int main(int argc, char** argv) { if (!SDL_Init(SDL_INIT_VIDEO)) { SDL_Log("Couldn't initialize SDL: %s", SDL_GetError()); return 1; (void)argc; (void)argv; SDL_assert(SDL_Init(SDL_INIT_VIDEO)); SDL_Window* window = SDL_CreateWindow(TARGET_NAME, 1280, 720, 0); SDL_assert(window); CreateGpuContext(window); bool running = true; while (running) { SDL_Event event; while (SDL_PollEvent(&event)) { switch (event.common.type) { case SDL_EVENT_QUIT: running = false; break; } } SDL_GPUCommandBuffer* commandBuffer = SDL_AcquireGPUCommandBuffer(gContext.mDevice); if (!commandBuffer) { SDL_Log("AcquireGPUCommandBuffer failed: %s", SDL_GetError()); continue; } SDL_GPUTexture* swapchainTexture; if (!SDL_WaitAndAcquireGPUSwapchainTexture(commandBuffer, gContext.mWindow, &swapchainTexture, NULL, NULL)) { SDL_Log("WaitAndAcquireGPUSwapchainTexture failed: %s", SDL_GetError()); continue; } SDL_GPUColorTargetInfo colorTargetInfo; SDL_zero(colorTargetInfo); colorTargetInfo.texture = swapchainTexture; colorTargetInfo.load_op = SDL_GPU_LOADOP_CLEAR; colorTargetInfo.store_op = SDL_GPU_STOREOP_STORE; colorTargetInfo.clear_color.r = 0.2f; colorTargetInfo.clear_color.g = 0.2f; colorTargetInfo.clear_color.b = 0.85f; colorTargetInfo.clear_color.a = 1.0f; SDL_GPURenderPass* renderPass = SDL_BeginGPURenderPass( commandBuffer, &colorTargetInfo, 1, NULL ); SDL_EndGPURenderPass(renderPass); SDL_SubmitGPUCommandBuffer(commandBuffer); } SDL_Log("Everything is working."); DestroyGpuContext(); SDL_Quit(); return 0; } #ifdef __cplusplus } // end cpp_test #endif
Download the source for this example here.