The Developer’s Cry

a blog about computer programming

SDL3 For The Masses

Beginning this year, SDL3 reached stable maturity. I’ve been wanting to write about it since. In some ways SDL version 3 is more of the same SDL, while in another way it propels SDL into the 2020s, and makes SDL 2 look old and dated. Most notably is support for Vulkan and (Apple) Metal, but I won’t go into that here. The API has been cleaned up, overall it’s more consistent now. Porting SDL 2 games to version 3 can be a lot of work, but the good news is, you don’t have to, and probably you shouldn’t, because you have better things to do with your time. That said, I did port a few old SDL version 1 codes over to 3. What I’m going to do here is simply layout a basic SDL3 setup for absolute beginners.

Let’s make a window

Before creating the window, we should initialize SDL. There is this SDL_main thing that is SDL’s main function, but really, we just want to use regular main() as entrypoint. So, we’re just going to call SDL_SetMainReady() to indicate that we’re good.

If you are building a (mobile) app, you may want to structure it as an app, and use the callback system. Read the documentation about SDL_main if you want to know more. SDL_SetAppMetadata() is also about apps and integrating nicely with current day windowing environments and ecosystems.

As with older SDL, call SDL_Init() and pass SDL_INIT_xxx flags OR-ed together. A difference with older SDL is that these kind of functions now return bool rather than int.

Create the window and just use a global variable for storing the SDL window. There is nothing wrong with using global variables…! Especially not in game codes.

#include <stdio.h>
#include <stdlib.h>
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>

// classic fullHD resolution
const int window_w = 1920;
const int window_h = 1080;
SDL_Window* sdl_window = NULL;

int main(int argc, char* argv[]) {
    SDL_SetMainReady();
    SDL_SetAppMetadata("SDL3 test", "0.1.0", "com.mydomain.sdl3_test");
    if (!SDL_Init(SDL_INIT_VIDEO|SDL_INIT_EVENTS)) {
        fprintf(stderr, "error: SDL_Init: %s\n", SDL_GetError());
        exit(-1);
    }
    if ((sdl_window = SDL_CreateWindow("SDL3 test", window_w, window_h, 0)) == NULL) {
        fprintf(stderr, "error: failed to create window: %s\n", SDL_GetError());
        SDL_Quit();
        exit(-1);
    }

    // we will put more code here ...

    SDL_Quit();
    return 0;
}

We can already compile and run this, and nothing seems to happen. That’s because the program now quits directly after creating the window, and it happens so fast, you don’t see it. Add a sleep and the window will show.

Compiling & linking

You may have a question now, how to compile?

SDL3 works nicest with pkg-config.

gcc -Wall $(pkg-config --cflags sdl3) main.c \
    -o main $(pkg-config --libs sdl3)

In Makefile you can put something like:

CFLAGS += $(shell pkg-config --cflags sdl3)
LFLAGS += $(shell pkg-config --libs sdl3)

Drawing stuff

To draw anything at all, you need a renderer.

Rather than drawing directly to the window, we will render to a texture that serves as a “virtual” back-buffer. Then, at 60 fps (for example) we will stretch that texture over the window, and do letterboxing as needed. SDL can handle the letterboxing for us; this is called the logical presentation.

SDL_Renderer* sdl_renderer = NULL;

const int vscreen_w = 800;
const int vscreen_h = 600;
SDL_Texture* vscreen = NULL;

And add to main():

// create renderer
if ((sdl_renderer = SDL_CreateRenderer(sdl_window, NULL)) == NULL) {
    fprintf(stderr, "error: failed to create renderer: %s\n", SDL_GetError());
    SDL_Quit();
    exit(-1);
}
SDL_SetRenderVSync(sdl_renderer, 1);

// create backbuffer
if ((vscreen = SDL_CreateTexture(sdl_renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, vscreen_w, vscreen_h)) == NULL) {
    fprintf(stderr, "error: failed to create render target texture: %s\n", SDL_GetError());
    SDL_Quit();
    exit(-1);
}
SDL_SetTextureScaleMode(vscreen, SDL_SCALEMODE_LINEAR);

// set letterboxing
SDL_SetRenderLogicalPresentation(sdl_renderer, vscreen_w, vscreen_h, SDL_LOGICAL_PRESENTATION_LETTERBOX);

Note how the resolution of the window and the backbuffer (vscreen) do not need to match. In fact, it’s possible to render at a higher resolution and scale down. Also, you may change the resolution of the logical presentation to scale it to big chunky pixels.

For pretty pixel art SDL3 has a special scaling mode named SDL_SCALEMODE_PIXELART. It works nicely for pretty pixel art; if you want chunky retro-style pixels, stick with SDL_SCALEMODE_NEAREST.

In the following example we draw a red square on a dark blue background. The reason for using dark blue is so that you can see letterboxing in effect.

// render to backbuffer texture
SDL_SetRenderTarget(sdl_renderer, vscreen);

// clear screen with dark blue
SDL_SetRenderDrawColor(sdl_renderer, 0, 0, 128, 255);
SDL_RenderClear(sdl_renderer);

/*
    draw scene: a red square for now
*/
SDL_SetRenderDrawColor(sdl_renderer, 255, 0, 0, 255);
SDL_FRect r = {
    100.0f, 100.0f,
    400.0f, 400.0f
};
SDL_RenderFillRect(sdl_renderer, &r);

/*
    end draw scene

    Next, render backbuffer to the window
*/
SDL_SetRenderTarget(sdl_renderer, NULL);
SDL_RenderTexture(sdl_renderer, vscreen, NULL, NULL);
SDL_RenderPresent(sdl_renderer);

Note that SDL3 uses Frect; floating point rectangles, rather than pixels. You can still have your pixels, but the graphics dimensions will be in floats. The advantage of floats is that you can have more advanced graphics (even in 2D), for example with camera transformations.

Input events

SDL works with an event loop. Keyboard input, mouse clicks, even joystick or gamepad movement, those are all events. The main event loop is basically the same as in SDL version 2.

The recommended trick is to have a flag is_running that remains true as long as there has been no event that signals quitting the program.

It is well possible that multiple events occur in fast succession, even within a single frame. Therefore PollEvent must always be called in a loop, so that all pending events are handled.

SDL_ResetKeyboardState();

SDL_event event;
bool is_running = true;

while (is_running) {
    while (SDL_PollEvent(&event)) {
        switch (event.type) {
            case SDL_EVENT_QUIT:
                // window closed
                is_running = false;
                break;

            case SDL_EVENT_KEY_DOWN:
                if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
                    is_running = false;
                }
                break;

            default:
        }
    }

    /*
        Here you should render:
        (see also previous snippet)
        - select backbuffer
        - clear screen
        - render scene

        and finally, present that
    */

    SDL_RenderTexture(sdl_renderer, vscreen, NULL, NULL);
    SDL_RenderPresent(sdl_renderer);
}

Finally, present the rendered frame. RenderPresent will wait for the “vsync” (modern displays don’t actually have a vertical sync. But it will wait for the next frame to be displayed). If you didn’t set the RenderVSync earlier, then it will spin the cpu (and gpu!) to 100%. In that case you need another means of waiting, like SDL_WaitEventTimeout() to wait for the first next event.

That’s all, folks

This is the basics of SDL3. What can we do with this? Create Pac-Man. Or Tetris. Or Quake 9.

I will leave you with a link to the documentation: