This lesson covers the fundamental graphics technique of double buffering.
Understand why directly drawing frames can lead to visual artifacts, grasp the concept of using separate front and back buffers, and see how SDL simplifies buffer swapping with SDL_UpdateWindowSurface()
.
We'll modify our existing code to implement a proper rendering loop with buffer clearing and swapping.
In this lesson, we’ll be continuing to work on our main.cpp
and Window.h
files. Their current state is provided below.
#include <SDL.h>
#include "Window.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
SDL_Event Event;
while (true) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
}
}
return 0;
}
#pragma once
#include <iostream>
#include <SDL.h>
class Window {
public:
Window(){
SDLWindow = SDL_CreateWindow(
"Hello Window",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
800, 300, 0
);
SDL_FillRect(
GetSurface(),
nullptr,
SDL_MapRGB(
GetSurface()->format, 50, 50, 50
)
);
SDL_UpdateWindowSurface(SDLWindow);
}
SDL_Surface* GetSurface() {
return SDL_GetWindowSurface(SDLWindow);
}
Window(const Window&) = delete;
Window& operator=(const Window&) = delete;
~Window() {
if (SDLWindow && SDL_WasInit(SDL_INIT_VIDEO)) {
SDL_DestroyWindow(SDLWindow);
}
}
private:
SDL_Window* SDLWindow{nullptr};
};
In computer graphics, we create the illusion of motion by showing a sequence of still images in quick succession. These still images are typically called "frames", and rendering a frame is the final step on each iteration of our application loop:
#include <SDL.h>
#include "Window.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
SDL_Event Event;
while (true) {
// 1. Process Events
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
}
// 2. Update Objects
// ...
// 3. Render Frame
// ... // <h>
}
return 0;
}
A lot of complexity and clever algorithms can go into generating these frames. After all the processing, we are left with an image that we want to display on the player’s screen.
A buffer is another name for an area of memory where data is stored. In computer graphics contexts, buffers usually refer to frame buffers - blocks of memory that store color values that will be displayed on a screen. Conceptually, they’re just big arrays of pixels, similar to an SDL_Surface
.
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 pixels.
That presents a problem: we don’t want users to witness the frame being constructed. That would break the illusion, so instead, we create multiple buffers.
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 finish 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.
When we’re using SDL, the double buffering process is mostly automated. Conceptually, we can imagine our drawing operations, such as SDL_FillRect()
, being performed on the back buffer.
However, we need to inform SDL when we’re done rendering, so it can show our updates to the player, much like a buffer swap. To do that, we call SDL_UpdateWindowSurface()
, passing the pointer to the SDL_Window
.
Currently, the constructor in our Window
class is rendering a gray rectangle to the entire back buffer, and then immediately swapping buffers so we can see it:
// Window.h
// ...
class Window {
public:
Window(){
SDLWindow = SDL_CreateWindow(
"Hello Window",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
800, 300, 0
);
SDL_FillRect(
GetSurface(),
nullptr,
SDL_MapRGB(
GetSurface()->format, 50, 50, 50
)
);
SDL_UpdateWindowSurface(SDLWindow);
}
// ...
};
This works for now but, because this is being done in the Window
constructor, it only happens a single time. To prepare for future chapters, we need to render this rectangle and swap our frame buffers on every iteration of our application loop.
And, for reasons we’ll explain soon, we need to perform these two actions at different points in our application loop. So, let’s move this code out of our constructor and into two public functions. We’ll move the SDL_FillRect()
code into a function called Render()
, and the SDL_UpdateWindowSurface()
will move to Update()
:
// Window.h
// ...
class Window {
public:
Window(){
SDLWindow = SDL_CreateWindow(
"Hello Window",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
800, 300, 0
);
SDL_FillRect(
GetSurface(),
nullptr,
SDL_MapRGB(
GetSurface()->format, 50, 50, 50
)
);
SDL_UpdateWindowSurface(SDLWindow);
}
void Render() {
SDL_FillRect(
GetSurface(),
nullptr,
SDL_MapRGB(
GetSurface()->format, 50, 50, 50
)
);
}
void Update() {
SDL_UpdateWindowSurface(SDLWindow);
}
// ...
};
Let’s go back to our application loop and think about the rendering part a little more. The first thing we should note that the buffer we’re drawing on (the back buffer) will not be empty.
It still contains the content from the previous frame, from when it was the front buffer. If we don’t clear that content, our frame will end up being a mixture of content from multiple iterations of our application loop.
That’s not an issue right now because we’re just rendering a gray rectangle, but it will become an issue soon if we don’t fix it. So, let’s start each render pass by "clearing" the buffer. We do this by just filling the buffer with some solid background color.
That’s exactly what our window’s Render()
function does, so let’s call it:
#include <SDL.h>
#include "Window.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
SDL_Event Event;
while (true) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
}
// Render background color
GameWindow.Render();
}
return 0;
}
Now that we have a clean buffer filled with our background color, we render all of our other objects on top of this background. That is, we render our objects after we’ve cleared the buffer.
#include <SDL.h>
#include "Window.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
SDL_Event Event;
while (true) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
}
// Render background color
GameWindow.Render();
// Render everything else
// ...
}
return 0;
}
We don’t have any other objects yet, so, for now, let’s just move on to the next step.
After we’ve rendered everything in our scene, our back buffer is complete and ready to become the front buffer. We trigger the swap by calling our window’s Update()
function.
#include <SDL.h>
#include "Window.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
SDL_Event Event;
while (true) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
}
// Render background color
GameWindow.Render();
// Render everything else
// ...
GameWindow.Update(); // swap buffers
}
return 0;
}
Currently, we don’t have anything else to render, so there’s nothing between these two function calls. But, as we add objects, we’ll stick to this pattern. Each render pass starts by clearing the buffer with a solid color, and it ends by swapping the buffers. All of the rendering we do will be performed between those two actions.
You might wonder why we redraw the whole scene even for static images. While it seems counterintuitive, this "clear and redraw" strategy is the standard approach in real-time graphics.
Graphics processors are built to handle this workload efficiently.
Implementing logic to detect changes and only redraw specific portions can be error-prone and complex, often negating any potential performance gains. Consistency and simplicity in the rendering pipeline are usually preferred.
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.
Our final files, which we’ll continue to use throughout the course, are provided below:
#include <SDL.h>
#include "Window.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
SDL_Event Event;
while (true) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
}
GameWindow.Render();
GameWindow.Update();
}
SDL_Quit();
return 0;
}
#pragma once
#include <iostream>
#include <SDL.h>
class Window {
public:
Window() {
SDLWindow = SDL_CreateWindow(
"Hello Window",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
800, 300, 0
);
}
void Render() {
SDL_FillRect(
GetSurface(),
nullptr,
SDL_MapRGB(
GetSurface()->format, 50, 50, 50
)
);
}
void Update() {
SDL_UpdateWindowSurface(SDLWindow);
}
SDL_Surface* GetSurface() {
return SDL_GetWindowSurface(SDLWindow);
}
Window(const Window&) = delete;
Window& operator=(const Window&) = delete;
~Window() {
if (SDLWindow && SDL_WasInit(SDL_INIT_VIDEO)) {
SDL_DestroyWindow(SDLWindow);
}
}
private:
SDL_Window* SDLWindow{nullptr};
};
This lesson introduced the concept of double buffering to prevent visual artifacts like flickering during rendering. We learned about the front buffer (visible to the user) and the back buffer (where the next frame is drawn).
In SDL, drawing operations modify the back buffer, and SDL_UpdateWindowSurface()
swaps the buffers, making the back buffer visible. A standard rendering loop involves clearing the back buffer, drawing scene elements, and finally swapping the buffers.
Key Takeaways:
SDL_UpdateWindowSurface()
swaps the front and back buffers.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