So if you've come here, you're interested in learning SDL_GPU. As background, I should first say that I myself am not an expert in the topic of Graphics, or in SDL_GPU itself. I've written a few relatively simple renderers for games, imgui projects, and similar dabbling. I've mostly focused on the CPU side of things, and only recently have I been learning about some of the more "modern" approaches to rendering. When Evan Hemsley (also known as cosmonaut), principal designer of SDL_GPU, finishes his graphics book, please go buy it and read it!
So all that being said don't expect scene graphs, PBR models, raytracing, or any of that sort of thing. This isn't a replacement for formal graphics coursework or in-depth learning of everything you might need to know to read technical papers on the above topics. It's intent is to get beginners comfortable with the basics so they can begin to learn some of that outside of a formal setting. I can't give you a complete understanding of Linear Algebra, or the foundational knowledge to understand the rendering equation.
In the more broad sense, it's also not an SDL3 or C tutorial. Anytime I do something I consider somewhat interesting in SDL3 for a beginner, I'll try to briefly cover it. Those asides however, are not a replacement for the documentation. On the other hand, I will spend no time on C as a language or it's features. It's too difficult to try to combine learning a language, a library, and the basics of Graphics all at the same time. I wouldn't encourage it, and this tutorial would become much too heavy if it also had to cover pointers, arrays, macros, memory management, and the like. Similarly, while I expect the setup portion of this guide to increase in size, ultimately this is not a replacement for understanding your chosen tools, hence the assumption that you have a handle on the basics of Git, CMake, and your C/C++ toolchain.
The goal of this guide is to help you understand the API surface of SDL_GPU. Ideally we get you to the point where you're comfortable reading about techniques written in other APIs and applying them to this API. Most of these APIs do look at least roughly similar. OpenGL is a state machine, but a lot of that state is stuff you'd put into pipelines in SDL_GPU or the other lower level APIs. If you've used Vulkan, DX, or Metal, you'll see echoes of those in SDL_GPU.
As mentioned above, the CPU side code examples will be written in relatively simple C99 so that both C and C++ programmers can copy and paste and it works in both. For folks who love C, this does mean that I won't be using designated initializers, so apologies for that. Feel free to use them in your own code. It'll assume you know your language of choice and are familiar with the tooling. Regarding shaders, we'll be using HLSL by way of SDL_shadercross. As of writing, it's not stable yet, so things might change a bit, I'll endeavor to keep it up to date.
If you decide to use the template, there will be some short setup involving CMake, git, and some tools we'll download and install down below. I know CMake isn't for everyone, but for the sake of this tutorial I'll assume you're using it, or otherwise have your tooling figured out. I'll try to give you a pretty simple framework to work with in CMake so that you can just copy and paste it, and things will be easy from then on.
When we get there, I'll briefly explain how to use SDL_shadercross to compile shaders, but from then on will assume that you're comfortable with that. The template has this entirely automated on Windows, further work is needed for Mac, and some manual setup will be required from Linux.
First off, if you're already comfortable with the basics from another API, and just want to get a handle on SDL_GPU with some additional context beyond the documentation and samples repo, feel free to jump around. If you already know SDL, please skip the sections that cover new-to-beginners SDL calls.
Every major section of a chapter will have some notes at the bottom on API calls with links to the documentation for each of them. This supplements the text, and it's intended that you go look at the documentation yourself as well, even just to get used to looking it over. These notes might also sometimes be highly technical for folks interested in that sort of thing. If you find your eyes glossing over, maybe just skip the note for now and take a look at the documentation.
I cannot cover every aspect of every function hence this documentation reading encouragement. SDL is a living API, and while we likely won't cover them much, many of the APIs have alternate functions that take an SDL_PropertiesID for additional tweaking. These properties will evolve with time, so there may be new ones added after time of writing that I can't know about. We'll be using this functionality mostly for creating our own functions for making Graphics resources to ensure they're all named for the sake of debugging in tools like RenderDoc.
Something I don't like about some Graphics material I've read is writing literally everything in main. On the other hand examples being strewn around multiple files have their own problems. To a certain extent we can't get around that, given that shaders exist, and in the modern APIs, including SDL_GPU, you can't just provide text to the API, not to mention the portability problem. We could use something like SDL_shadercross as a library to compile our shaders at runtime, but suffice it to say, it's not a trivial integration.
So with that said, here's my strategy, based on one I've been using in a side project. We'll wrap our device, one time query information, and initialization in a Context struct that we use a few functions to bring up and bring down. This will be global, both because I don't think it's likely most applications will be doing multi-GPU work, and because it's just easier to not need to pass it around all the time.
With regards to techniques we implement during chapters, we'll make a per-technique "PipelineContext" struct and set of functions for creating/destroying/utilizing them. This is, quite frankly, intentionally vague. The point of these are not to be a perfect abstraction over Pipelines, Shaders, any of the Copy/Render/Compute passes, or whatever. They will sometimes be composable, and sometimes they won't. Some of them will need their own Render passes, and some of them can reuse an existing one that was already open. Some will be compute, render, or copy only. Some will be some combination of the three.
The goal of these is two fold.
One is that I know a lot of beginners to graphics have literally no idea how to group things together. I certainly didn't! I remember staring at some of the diagrams of the Vulkan "lifecycle", all the structs and functions and data flows and render passes to get things on the screen. Certainly I could follow it, but actually organizing them into something reasonable was impossible.
I won't pretend that these PipelineContexts are going to blow your mind or are ready for prime time in AA or AAA games. They should hopefully be instructive on how you can process your data, as reasonably efficient as we can make it without getting into the weeds.
Selfishly the other reason is it helps me to organize these chapters and samples. If we want to go back and extend a previous example, we can go grab it and be off to the races. And at least personally I dislike when my scopes have access to a bunch of data that really isn't relevant for the task at hand. These contexts will begin seemingly pretty granular, but they'll evolve to processing a lot of objects at once down the line, managing their upload, manipulation of the data in compute, and executing on rendering that data.
So first, go install some stuff if you haven't already:
To make things as simple as possible, I've made a sample repo for you to be able to start working immediately, assuming you have all your tools configured.
git clone --recurse-submodules -j8 https://github.com/playmer/sdl_gpu_by_example.git cd sdl_gpu_by_example/code cmake -B build cmake --build build
You should now have an exceedingly basic SDL program within the bin directory, as well as compiled versions of all our examples. Ideally you should execute it from the root of the repository. This will become more important when we start using assets. The CMake build command should have told you toward the end of execution where it built the executable. For example, with the Visual Studio generator I would execute it like so:
bin\Debug\sdl_by_example.exe
Were I using the make or Ninja generators it would likely look like this:
bin\sdl_by_example.exe
On Linux and MacOS, the executables wouldn't have the .exe extension.
Once executed you should see that it prints "Everything is working." when executed. If you don't see that, there should at least be an error that goes like "Couldn't initialize SDL: [Some error reported by SDL]". If you don't see that then there's likely something wrong with accessing the SDL shared library. Possibly an RPATH issue if you're on Linux or similar.
To briefly explain the anatomy of the repo you've cloned and built:
target_sources, but it should be as easy as adding in some files to the template folder and adding the c/cpp (and optionally headers as well, it helps with IDE projects), files to the target_sources call inside of the CMakeLists.txt with the folder.external libraries (at time of writing, just SDL itself)source, where we'll put the code, Shaders, and CMake scripts we use to build the template you just executed, and the examples seen in later chapters.
source/01_template. The intent is for you to use this as a workspace to work through the examples. You can also easily create copies of this folder within the source folder with different names, so that you may work through each example, keeping your old code around.If you do that you'll need to make sure to add your folder to the list of add_subdirectory calls inside of source/CMakeLists.txtAssets where we'll place things like images, models, and compiled shaders.bin, a folder for ease of use, this is where we've placed built executables and libraries via some CMake variables.tools, a folder where we'll place some binaries of our tools, at time of writing, I only really expect to store SDL_shadercross in here, but you never know.Finally, lets take a look at one of the most basic SDL programs we can make, which if you chose to follow along with the examples and use the template, was one of the things you compiled up above.
So lets pull up the code we just compiled in the template and take a look at it:
#include <SDL3/SDL.h> #include <SDL3/SDL_main.h> int main(int argc, char** argv) { if (!SDL_Init(SDL_INIT_VIDEO)) { SDL_Log("Couldn't initialize SDL: %s", SDL_GetError()); return 1; } SDL_Log("Everything is working."); SDL_Quit(); return 0; }
Now, this isn't a tutorial on SDL itself, but I'll try to briefly cover things as we add them.
SDL3/SDL.h is the canonical way to include SDL in version 3, and we do indeed generally recommend that, because it includes almost everything SDL provides. We can see that all we're really doing is trying to initialize SDL via SDL_Init with the Video system in particular active. If it fails, we print out an error and return out, otherwise we print a message to let the user know things are okay. And then finally we call SDL_Quit to tear down anything we or SDL has left over.
A note on SDL_main and the callbacks
One of the few headers
SDL3/SDL.hdoesn't include isSDL3/SDL_main.h, which is a header only library that implementsint main(int argc, char** argv)for you. Now you may notice that we've defined that function. WellSDL_main.halso provides a macro that turnsmainintoSDL_main, so that's what we're actually implementing, and what themainthat SDL implements calls. You can read more about it in SDLs main functions README. For simplicities sake, we're not currently using the callbacks system provided by SDL_main so as not to confuse folks from non C and C++ languages reading this tutorial, but know that it's highly encouraged for good reason.
Now that we've covered the basics, lets get us a Window and an Event loop.
SDL_Init
SDL_Quit
SDL_GetError
SDL_Log
SDL.h hands it to us, but it's technically more portable for some situations and platforms. Were we in C++, I'd probably consider std::format or the fmt library of which it's based due to it's superior formatting options.SDL_main and the main functions
Download the source for this example here.