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
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:
Get info about SDL from:
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 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
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
/* 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
... 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
#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
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
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
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
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
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...")
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
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
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
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
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
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')
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
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
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
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
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
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
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