Become a software engineer with C++. Starting from the fundamentals, we guide you step by step along the way, finishing off with a portfolio-ready capstone project.
Updated - 9 minute read
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, we simulate movement by quickly showing a sequence of still images, typically called “frames”.
A lot of complexity and elaborate algorithms can go into generating these frames. But, after all the processing, we’re just with a collection of pixel colors we want to show the user.
A buffer is an area of memory where data is stored for a short period of time. Frames are buffers - we can think of them as big arrays of pixel objects. Our goal boils down to determining which color each pixel needs to be, and to write those values into the buffer.
The problem is that we can’t do that all at once. It takes many statements, function calls and calculations to generate a frame, and they don’t all complete at the same time.
That presents a problem for our users - we don’t want them to witness the frame being constructed. That would shatter the illusion. So, we invented double buffering.
With double buffering, we have, predictably, two buffers. There’s the one 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 completely 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);
std::swap(Front, Back);
}
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
.
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
after making our changes.
In our example application loop from the previous lesson, we were calling this function as the final step of the loop. This ensures our buffers are swapped as quickly as possible, maximising our frame rate.
It's generally beyond the scope of this course, but as an aside for anyone who has heard the phrases, and wants some more context on what they are. These two concepts are inherently linked to the multiple buffer setup we're discussing here.
Once our software implements double buffering, there’s an unpleasant interaction that can happen with our hardware.
This is because computer monitors don’t update their display all at once - that is also a process that takes some time. If we trigger our swap in the middle of that process, part of the monitor can show one frame, whilst another part shows a different frame. This is what causes screen tearing.
It is also why screen tearing is more common at higher frame rates. Higher frame rates means more frequent swaps, which means more swaps happening at the same time the monitor is refreshing.
There are various options for dealing with this, a popular one being synchronizing the application’s refresh rate with the hardware 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 an interesting problem for us. When we complete the next frame, it needs to wait in the back buffer until the hardware is ready for it.
So, we’re left sitting with a completed frame in both our buffers. Until the hardware requests the swap, we can’t start working on our next frame. Predictably, this is the use case for adding yet another buffer to the process. With triple buffering, we have:
Become a software engineer with C++. Starting from the fundamentals, we guide you step by step along the way, finishing off with a portfolio-ready capstone project.