nullprogram.com/blog/2023/01/08/
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.
Caveats:
-
Some circumstances require special treatment, and
sdl2-config
may be too blunt a tool. That’s fine, but generally prefersdl2-config
as the default approach. -
The script does not support extensions such as
SDL2_image
. These require their own, separate configuration (viapkg-config
, etc.). Personally I don’t think they’re worth the trouble, such as, for example, when you can use stb, or QOI instead of PNG. -
There’s an alternative build option using CMake, without any use of
sdl2-config
, but I won’t discuss it here.
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:
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
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:
-
fprintf
is not part of the SDL platform. This is going behind SDL’s back, reaching around the abstraction to a different platform. Strictly speaking, this API may not even be available to an SDL application. -
SDL applications are graphical, so
stderr
is likely disconnected from anything useful. Few would ever see this message.
Fortunately SDL provides two alternatives:
-
SDL_Log
: like Cprintf
, but SDL will strive to connect it to somewhere useful. If the application was launched from a terminal or console, SDL will find it and hook it up to the logger. On Windows, if there’s a debugger attached, SDL will use OutputDebugString to send logs to the debugger. -
SDL_ShowSimpleMessageBox
: using any means possible, attempt to display a message to the user. LikeSDL_Log
, it’s safe to use before/without initializing SDL subsystems.
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:
-
Indicates mandatory properties of the renderer. Examples: “must use accelerated rendering,” “must use software rendering,” “must support vertical synchronization (vsync).” Drivers without the chosen properties are skipped.
-
If
SDL_RENDERER_PRESENTVSYNC
is set, also enables vsync in the created render.
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:
SDL_PollEvent
with vsyncSDL_WaitEvent
without vsync
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.
from Hacker News https://ift.tt/GT8y1kB
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.