SDL

Graphics

Graphics programming is not essential for this unit, even for the sketch assignment, but it is much more satisfying if you can use it, so these slides give you an overview of graphics programming using the SDL library

They concentrate on the things that are most difficult to find out from the documentation or tutorials

The SDL library

SDL (Simple DirectMedia Layer) is one of the simplest graphics libraries for C (but use version 2, not version 1)

Other libraries focus too much on C++, or on complex approaches, but SDL is still not truly simple to use

Beware tutorial sites:

SDL Info

Get info about SDL from:

Installing

On Linux, SDL may be installed already, or you can use apt - make sure you get version 2

On MacOS, SDL may be installed already, or you can use brew - make sure you get version 2

On Windows, if you can't dual boot Linux or use a virtual machine, you can use Cygwin: use its installer to get libSDL2-devel, see aside: computers

Graphics under Cygwin is not entirely satisfactory - it is slow and clunky - MSYS2 is better, but then there are lots of other issues to deal with

SDL scope

SDL covers: key presses, mouse clicks & movements, simple 2D graphics, pixels, rectangles, bmp images, audio (if you are lucky), timers (and threads - avoid)

Extensions cover: text (SDL2_ttf), extra image types (SDL2_image), networking (SDL2_net), extra audio (SDL2_mixer)

It doesn't cover 3-D, shapes, splines, filters, lighting, GUIs, menus..., drag-and-drop, physics, collisions

Blue sky programming

Let's look at a minimal demo to sort out basics

It creates a sky-blue window

Then it waits for ten seconds

Then it shuts down

The blue sky program 1

/* Display blue sky for 10 seconds. */

#define SDL_MAIN_HANDLED
#include <SDL2/SDL.h>

// Fail, printing the SDL error message, and stop the program.
void fail() {
    fprintf(stderr, "%s\n", SDL_GetError());
    SDL_Quit();
    exit(1);
}

// Check the results from SDL functions for errors.
int I(int n) { if (n < 0) fail(); return n; }
void *P(void *p) { if (p == NULL) fail(); return p; }
...

This part sets up error handling

The blue sky program 2

...
int main() {
    int w=640, h=480;
    I(SDL_Init(SDL_INIT_VIDEO));
    SDL_Window *window = P(SDL_CreateWindow("Sky", 100, 100, w, h, 0));

    SDL_Surface *surface = P(SDL_GetWindowSurface(window));
    Uint32 skyBlue = SDL_MapRGB(surface->format, 100, 149, 237);
    I(SDL_FillRect(surface, NULL, skyBlue));
    I(SDL_UpdateWindowSurface(window));

    SDL_Delay(10000);
    SDL_Quit();
    return 0;
}

This part creates the window and displays it for 10 seconds

Main

#define SDL_MAIN_HANDLED

This is only needed on Windows (e.g. MSYS2)

Native Windows programs must start with WinMain, and SDL has a renaming mechanism to allow you to use main, but gcc on Windows already solves the problem, so this line switches off SDL's renaming

Configuration

Sometimes SDL is configured so that you use

#include <SDL.h>

instead of

#include <SDL2/SDL.h>

That's not right, because then you can't specify different versions if they are both installed, but you may have to put up with it

Compiling

To compile the program in the lab or under Cygwin or other 'standard' environment:

clang -std=c11 -Wall -o sky sky.c -lSDL2

The option -lSDL2 must come after sky.c, to link with the SDL library

Then to run the program, type:

./sky

If you get a blue window for a few seconds, all is well

Compiling alternatives

To compile the program in a 'non-standard' environment:

clang ... sky.c `pkg-config --cflags --libs sdl2`

This uses the pkg-config command (which you may have to install) to find out where SDL has been installed and tell gcc, or if that fails try:

clang ... sky.c `sdl2-config --cflags --libs`

The backquotes execute the command and insert the result into clang's options

Error handling

Most of the function calls in the SDL library return a NULL pointer or negative integer to indicate an error

It is tempting to ignore errors while you experiment

But it is precisely when you are experimenting that you need the most feedback from the program when things don't work, and anyway you want to avoid developing bad habits, so error handling is essential

Verbose style

However, this style is too verbose and repetitive:

int r = SDL_Init(SDL_INIT_VIDEO);
if (r < 0) {
    // print and exit
}
SDL_Window *window = SDL_CreateWindow(...);
if (window == NULL) {
    // print and exit
}

It makes the program less readable: the error handling interrupts the main story line ("Once upon a time, where time is a measure of progress, there was a frog...")

Compromise

This style is not too bad:

I(SDL_Init(SDL_INIT_VIDEO));
SDL_Window *window = P(SDL_CreateWindow(...));

The functions I and P are ones you write to detect an error, report it, and exit

The function I is for SDL functions which return an integer which should be non-negative, and P is for ones returning a pointer which should be non-null

Reporting

Here's what the error functions look like:

void fail() {
    fprintf(stderr, "%s\n", SDL_GetError());
    SDL_Quit();
    exit(1);
}
int I(int n) { if (n < 0) fail(); return n; }
void *P(void *p) { if (p == NULL) fail(); return p; }

An SDL error creates an error message in a global variable, which SDL_GetError picks up

Starting up

Here's how to start up SDL:

I(SDL_Init(SDL_INIT_VIDEO));

Tutorials tell you to use SDL_INIT_EVERYTHING, but that initializes every device that SDL supports, and computers rarely have all those devices attached, so it usually fails

Shutting down

Here's how to shut down:

SDL_Quit();

This is only necessary if you have made 'sticky' changes to the computer, such as changing the screen resolution, but it is best to play safe

Window

Here's how to create a window:

SDL_Window *window = P(SDL_CreateWindow("Sky", 100, 100, w, h, 0));

The arguments are the title to go in the title bar, the position on screen, the width and height, and extra options

CPU and GPU

You may know that a computer has a GPU (Graphics Processing Unit, used to be called a 'graphics card') as well as a CPU (Central Processing Unit, usually just called 'processor')

Capabilites

A GPU is special purpose - it can do coordinate arithmetic to draw lots of 3D coloured or textured polygons quickly

The CPU is general purpose, and is better at unusual graphics operations such as applying image filters, and non-graphics work

SDL functions

There are two sets of SDL drawing functions, according to whether you want the CPU or GPU to do the work

The CPU functions are mainly designed for sprite programming, so they are limited to simple things like copying images onto the screen and handling keyboard and mouse events

The GPU functions are limited by what the GPU provides

You need to install extension libraries, or implement things yourself, to go further

Renderers and textures

If an SDL function makes any mention of Renderer or Texture, in the name or the argument types, then it is a GPU function

If it doesn't, then it is a CPU function

Painting

To paint the window blue using CPU functions:

SDL_Surface *surface = P(SDL_GetWindowSurface(window));
Uint32 skyBlue = SDL_MapRGB(surface->format, 100, 149, 237);
I(SDL_FillRect(surface, NULL, skyBlue));
I(SDL_UpdateWindowSurface(window));

To paint the window blue using GPU functions:

SDL_Renderer *r = P(SDL_CreateRenderer(window, -1, 0));
I(SDL_SetRenderDrawColor(r, 100, 149, 237, 255));
I(SDL_RenderClear(r));
SDL_RenderPresent(r);

The two sets of functions have totally different styles

Jargon

A surface in SDL is an image, stored as a 2D array of pixels in CPU memory

It is used on the CPU side as a 'context', i.e. an environment to draw in

When an image is stored in GPU memory, it is called a texture

A renderer is a GPU context, i.e. an environment in which to do GPU operations

Double buffering

You don't want users to see pictures being painted, or they look jittery

So you draw to somewhere off-screen, then make it visible on screen when you have finished

UpdateWindowSurface copies an off-screen surface into the window

RenderPresent swaps between two implicit pixel arrays, the one currently displayed and the one currently being drawn to

Frames

So the general strategy is to redraw the entire scene every 1/60 second or every time something changes

The picture created each time is called a frame

For a smooth result, you can synchronise the display of frames with the refresh rate of the user's screen

This can only be done on the GPU side, by giving the VSYNC option to CreateRenderer, after which calling RenderPresent includes an implicit synchronising delay