In the previous lesson, we described a typical game loop:
The transition from step 2 to step 3 warrants some further explanation. There is some theory here that is very important to understanding how SDL, and real-time graphics in general, works.
In computer graphics, movement is simulated by quickly showing a sequence of still images, typically called "frames".
A lot of complexity and clever algorithms can go into generating these frames. After all the processing, we are left with a collection of pixel colors that we want to display to the user.
A buffer is an area of memory where data is stored for a short period. Frames are buffers - we can think of them as big arrays of pixels. Each pixel represents a color by combining integers representing the red, green, and blue components:
struct Pixel {
int Red;
int Green;
int Blue;
}
std::vector<Pixel> Buffer;
The fundamental goal of computer graphics is to determine which color each pixel needs to be, and to write those values into the buffer.
However, this process takes time. It can take many statements, function calls, and calculations to determine the color of a single pixel, and a typical buffer can have millions of such pixels.
That presents a problem: we don’t want users to witness the frame being constructed. That would break the illusion, so instead, we came up with double buffering.
With double buffering, as expected, there are two buffers. There’s the buffer the user is seeing, and the one we’re building to show to them next.
The buffer the user is seeing is called the front buffer, and the one we’re building is the back buffer. When we’ve finished building the back buffer, we swap them around.
The back buffer becomes the front buffer and is shown to our users. The front buffer becomes the back buffer, and we start creating our next frame in it.
This is typically managed on the GPU but, were we to create the effect in C++, we could imagine it being something like this:
using Buffer = std::vector<Pixel>;
Buffer A;
Buffer B;
Buffer* Front { &A };
Buffer* Back { &B };
while(true) {
DrawEnvironment(Back);
DrawCharacters(Back);
DrawUI(Back);
Swap(Front, Back);
}
The using
statement in the first line of this code is an example of a type alias. It allows us to create a friendlier, alternative name for a more complex type. We cover type aliases in more detail in the next module.
In SDL2, the double buffering is mostly handled for us. When we use commands like SDL_FillRect()
, SDL ensures this is being done on the back buffer.
However, we do need to tell SDL when it needs to swap buffers. When we’re working with window surfaces, that command is SDL_UpdateWindowSurface()
, which we’ve been using in our Window
class in previous lessons.
If we’re making changes in our application, and those changes do not seem to be appearing, the first thing we should do is make sure we’re calling SDL_UpdateWindowSurface()
at the appropriate times.
In our example application loop from the previous lesson, we were calling this on every iteration of our loop.
These two topics are generally beyond the scope of this course, but they closely relate to the concept of graphics buffers, and may be of interest to those who want some more context on what they are.
Once our software implements double buffering, there’s an additional layer of complexity introduced by hardware constraints. Computer monitors also don’t update their display all at once.
If we trigger our buffer swap at the same time the monitor is updating, content from two or more frames can be shown on the screen at once. This is commonly called "screen tearing".
There are various options for dealing with this, a popular one being synchronizing the application’s refresh rate with the monitor’s refresh rate. Variations of this idea are commonly called vertical sync (VSync).
When our refresh rate is synchronized with the hardware’s refresh rate, this presents yet another problem for us. When we complete the next frame, it needs to wait on the back buffer until the monitor is ready for it.
So, we have completed frames in both our buffers. We’re ready to start working on a new frame, but we have nowhere to create it. Predictably, this is the use case for adding a third buffer to the process. With triple buffering, we have:
This is the reason VSync-based implementations can sometimes feel less responsive. They can have increased input latency - the time between the user performing an input, and that input being reflected on the screen.
We react to user inputs on the back buffer. With double buffering, that means those inputs are reflected on the next frame. With triple buffering, there’s potentially an additional frame (the middle buffer) between what is currently on screen, and the frame that implements the user’s input.
This lesson provides an overview of double buffering - the key mechanism that allows for real-time graphics. Double buffering involves two images, or buffers: a front buffer for display and a back buffer for rendering the next frame. Here are the key points covered:
SDL_UpdateWindowSurface()
to manage buffer swaps.Learn the essentials of double buffering in C++ with practical examples and SDL2 specifics to improve your graphics projects
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games