SDL2 common mistakes and how to avoid them

This article was discussed on reddit.

SDL has grown on me over the past year. I didn’t understand its value until viewing it in the right lens: as a complete platform and runtime replacing the host’s runtime, possibly including libc. Ideally an SDL application links exclusively against SDL and otherwise not directly against host libraries, though in practice it’s somewhat porous. With care — particularly in avoiding mistakes covered in this article — that ideal is quite achievable for C applications that fit within SDL’s feature set.

SDL applications are always interesting one way or another, so I like to dig in when I come across them. The items in this article are mistakes I’ve either made myself or observed across many such passion projects in the wild.

Mistake 1: Not using sdl2-config

This shell script comes with SDL2 and smooths over differences between platforms, even when cross compiling. It informs your compiler where to find and how to link SDL2. The script even works on Windows if you have a unix shell, such as via w64devkit. Use it as a command substitution at the end of the build command, particularly when using --libs. A one-shot or unity build (my preference) looks like so:

$ cc app.c $(sdl2-config --cflags --libs)

Or under separate compilation:

$ cc -c app.c $(sdl2-config --cflags)
$ cc app.o $(sdl2-config --libs)

Alternatively, static link by replacing --libs with --static-libs, though this is discouraged by the SDL project. When dynamically linked, users can, and do, trivially substitute a different SDL2 binary, such as one patched for their system. In my experience, static linking works reliably on Windows but poorly on Linux.

Alternatively, use the general purpose pkg-config. Don’t forget eval!

$ eval cc app.c $(pkg-config sdl2 --cflags --libs)

I wrote a pkg-config for Windows specifically for this case.

Caveats:

Mistake 2: Including SDL2/SDL.h

A lot of examples, including tutorials linked from the official SDL website, have SDL2/ in their include paths. That’s because they’re making mistake 1, not using sdl2-config, and are instead relying on Linux distributions having installed SDL2 in a place coincidentally accessible through that include path.

This is annoying when SDL2 not installed there, or if I don’t want it using the system’s SDL2. Worse, it can result in subtly broken builds as it mixes and matches different SDL installations. The correct SDL2 include is the following:

#include "SDL.h"

Note the quotes, which helps prevent picking up an arbitrary system header by accident. When carefully and narrowly targeting SDL-the-platform, this will be the only “system” include anywhere in your application.

Mistake 3: Not surrendering main

A conventional SDL application has a main function defined in its source, but despite the name, this is distinct from C main. To smooth over platform differences, SDL may rename the application’s main to SDL_main and substitute its own C main. Because of this, main must have the conventional argc/argv prototype and must return a value. (As a special case, C permits main to implicitly return 0, so it’s an easy mistake to make.)

With this in mind, the bare minimum SDL2 application:

#include "SDL.h"

int main(int argc, char **argv)
{
    return 0;
}

Caveat: Like with sdl2-config, some special circumstances require control over the application entry point — see SDL_MAIN_HANDLED and SDL_SetMainReady — but that should be reserved until there’s a need.

One such special case is avoiding linking a CRT on Windows. In principle it’s this simple:

#include "SDL.h"

int WinMainCRTStartup(void)
{
    SDL_SetMainReady();
    // ...
    return 0;
}

Then it’s the usual compiler and linker flags:

$ cc -nostdlib -o app.exe app.c $(sdl2-config --cflags --libs)

This will create a tiny .exe that doesn’t link any system DLL, just SDL2.dll. Quite platform agnostic indeed!

$ objdump -p app.exe | grep -Fi .dll
        DLL Name: SDL2.dll

Alas, as of this writing, this does not work reliably. SDL2’s accelerated renderers on Windows do not clean up properly in SDL_QuitSubSystem nor SDL_Quit, so the process cannot exit without calling ExitProcess in kernel32.dll (or similar). This is still an open experiment.

Mistake 4: Using the SDL wiki for API documentation

The SDL wiki is not authoritative documentation, merely a convenient web-linkable — and downloadable (see “offline html”) — information source. However, anyone who’s spent time on it can tell you it’s incomplete. The authoritative API documentation is the SDL headers, which fortunately are already on hand for building SDL applications. The SDL maintainers themselves use the headers, not the wiki.

If, like me, you’re using ctags, this is actually good news! With a bit of configuration, you can jump to any bit of SDL documentation at any time in your editor, treating the SDL headers like a hyperlinked wiki built into your editor. Just like building, sdl2-config can tell ctags where find those headers:

$ ctags -a -R --kinds-c=dept $(sdl2-config --prefix)/include/SDL2

I’m using -a (--append) to append to the tags file I’ve already generated for my own program, -R (--recurse) to automatically find all the headers, and --kinds-c=dept capture exactly the kinds of symbols I care about — #define, enum, prototypes, typedef — no more no less.

In Vim I CTRL-] over any SDL symbol to jump to its documentation, and then I can use it again within its documentation comment to jump further still to any symbols it mentions, then finally use the jump or tag stack to return. As long as I have t in 'complete' ('cpt'), which is the default, I can also “tab”-complete any SDL symbol using the tags table. There are a few rough edges here and there, but overall it’s a solid editing paradigm.

By the way, with sdl2-config in your $PATH, all the above works out of the box in w64devkit! That’s where I’ve mostly been working with SDL.

Mistake 5: Using stdio streams

A common bit of code in real SDL programs and virtually every tutorial:

if (SDL_Init(...)) {
    fprintf(stderr, "SDL_Init(): %s\n", SDL_GetError());
    return 1;
}

This is not ideal:

Fortunately SDL provides two alternatives:

If you’re paranoid, you could even use both:

if (SDL_Init(...)) {
    SDL_ShowSimpleMessageBox(
        SDL_MESSAGEBOX_ERROR, "SDL_Init()", SDL_GetError(), 0
    );
    SDL_Log("SDL_Init(): %s", SDL_GetError());
    return 1;
}

Though note that SDL_ShowSimpleMessageBox can fail, which will set a new, different error message for SDL_Log!

There’s a similar story again with fopen and loading assets. SDL has an I/O API, SDL_RWops. It’s probably better than the host’s C equivalent, particularly with regards to paths. If you’re not already embedding your assets, use the SDL API instead.

Mistake 6: Using SDL_RENDERER_ACCELERATED

This flag — and its surrounding bit set, SDL_RendererFlags — are a subtle design flaw in the SDL2 API. Its existence is misleading, causing to widespread misuse. It does not help that the documentation, both header and wiki, is incomplete and unclear. The SDL_CreateRenderer function accepts a bit set as its third argument, and it serves two simultaneous purposes:

The common mistake is thinking that this bit indicates preference: “prefer an accelerated renderer if possible”. But it really means “accelerated renderer or bust.”

Given a zero for renderer flags, SDL will first attempt to create an accelerated renderer. Failing that, it will then attempt to create a software renderer. A software renderer fallback is exactly the behavior you want! After all, this fallback is one of the primary features of the SDL renderer API. This is so straightforward there are no caveats.

Mistake 7: Not accounting for vsync

For a game, you probably ought to enable vsync in your renderer. The hint: You’re using SDL_PollEvent in your main event loop. Otherwise you will waste lots of resources rendering thousands of frames per second. If my laptop fan spins up running your SDL application, it’s probably because you didn’t do this. The following should be the most conventional SDL renderer configuration:

r = SDL_CreateRenderer(window, -1, SDL_RENDERER_PRESENTVSYNC);

The software renderer supports vsync, so it will not be excluded from the driver search when vsync is requested.

That’s only for SDL renderers. If you’re using OpenGL, set a non-zero SDL_GL_SetSwapInterval so that SDL_GL_SwapWindow synchronizes. For the other rendering APIs, consult their documentation. (I can only speak to SDL and OpenGL from experience.)

Caveat: Beware accidentally relying on vsync for timing in your game. You don’t want your game’s physics to depend on the host’s display speed. Even the pros make this mistake from time to time.

However, if you’re not making a game – perhaps instead an IMGUI application without active animations — there’s a good chance you don’t need or want vsync. The hint: You’re using SDL_WaitEvent in your main event loop.

In summary, graphical SDL applications fall into one of two cases:

Mistake 8: Using assert.h instead of SDL_assert

Alright, this one isn’t so common, but I’d like to highlight it. The SDL_assert macro is fantastic, easily beating assert.h which doesn’t even break in the right place. It uses SDL to present a user interface to the assertion, with support for retrying and ignoring. It also works great under debuggers, breaking exactly as it should. I have nothing but praise for it, so don’t pass up the chance to use it when you can.

While I’m at it: during developing and testing, always always always run your application under a debugger. Don’t close the debugger, just launch through it again after rebuilding. Also, enable UBSan and ASan when available for the extra assertions.

SDL wishlist

For months I had wondered why SDL provides no memory allocation API. I’m fine if it doesn’t have a general purpose allocator since I just want to grab a chunk of host memory for an arena. However, SDL does have allocations functions — SDL_malloc, etc. I didn’t know about them until I stopped making mistake 4.

It was the same story again with math functions: I’d like not to stray from SDL as a platform, but what if I need transcendental functions? I could whip up crude implementations myself, but I’d prefer not. SDL has those too: SDL_sin, etc. Caveat: The math.h functions are built-ins, and compilers use that information to better optimize programs, e.g. cool stuff like -mrecip, or SIMD vectorization. That cannot be done with SDL’s equivalents.

I’m surprised SDL has no random number generator considering how important it is to games. Since I prefer to handle this myself, I don’t mind that so much, but it does leave a lot of toy programs out there calling C rand. I would like SDL if provided a single, good seed early during startup. There isn’t even a wall clock function for the classic srand(time(0)) seeding event! My solution has been to mix event timestamps into the random state:

static Uint32 rand32(Uint64 *);

Uint64 rng = 0;
for (SDL_Event e; SDL_PollEvent(&e);) {
    rng ^= e.common.timestamp;
    rand32(&rng);  // stir
    switch (e.type) { /* ... */ }
}

As I learn more in the future, I may come back and add to this list. At the very least I expect to use SDL increasingly in my own projects.

Have a comment on this article? Start a discussion in my public inbox by sending an email to ~skeeto/public-inbox@lists.sr.ht [mailing list etiquette] , or see existing discussions.

null program

Chris Wellons

wellons@nullprogram.com (PGP)
~skeeto/public-inbox@lists.sr.ht (view)