Retro Game Programming Tips, Part 2: Double Buffering with Page Switching

Introduction

In this second part of our retro game programming series, we'll look at implementing double buffering with page switching. Double buffering is a technique where you draw your game screen into a buffer, and then quickly copy the entire buffer onto the screen to prevent flickering. If the computer supports multiple video memory pages then your buffer is the next page and the switch is implemented in hardware, costs no additional CPU cycles, and is very fast.

In this article, we'll assume two pages and the ability to tell the system which page is currently being drawn to and which page is currently displayed. We'll then alternate between the drawing page and the display page to implement the switching.

Platform Specifics

Let's start our journey by defining our platform-specific functions.

The function gsetpage(pg_type, pg_no) sets the current page. Valid values for pg_type are PG_DISPLAY for the page currently displayed, and PG_WRITE for the page to which all graphical operations go. These two values are bitmasks, so you can use them together. In other words, you can call the function like this: gsetpage(PG_DISPLAY|PG_WRITE, 0). Since we're assuming we only have two pages, the valid values for pg_no are 0 and 1.

Function gsetcolor(color) accepts only two values: CO_FORE for foreground color, and CO_BACK for background color. Drawing with the background color effectively erases the content. Finally, function gcls() clears the write page.

Now let us use all of them to write the clear screen function.

void clear_screen()
{
    gsetpage(PG_WRITE,0);
    gcls();
    gsetpage(PG_WRITE,1);
    gcls();
    gsetpage(PG_DISPLAY,0);
    gsetcolor(CO_FORE);
}

The Game State

We need a structure to store our game state. For simplicity, let's assume that we are only going to draw one sprite. Our game state structure, therefore, needs at least three members: the current display page, the current sprite position, and the previous sprite position.

Why do we need the previous sprite position? Because we will be drawing to a different page each cycle. And when we draw the next page, we will need to erase the previous content. The previous coordinates will help us with this task.

Our game structure will look like this:

typedef struct game_s {
    uint8_t page;
    int x;
    int y;
    int prevx;
    int prevy;
}

Following figure demonstrates our algorithm.

Implementation

We are now finally ready to implement our game loop. We will start by initializing the game structure. Then we will enter the loop and alternately display the current page and prepare the next page for displaying. We will use data from the previous page for content removal and data from the current page for calculating the next sprite position.

void game_loop() {

    /* remember our clear screen? */
    clear_screen();

    /* initialize game at position 0,0 */
    game_t g = { 0, 0, 0, 0, 0 };

    /* first display page to 1, but write page to 0 */
    gsetpage(PG_DISPLAY,1);
    gsetpage(PG_WRITE,g.page);

    /* is there previous game state? */
    bool has_prev_state = false;

    while(1) {

        /* set color to foreground */
        gsetcolor(CO_FORE);
        
        /* you need to provide function draw_sprite to draw sprite
           at position x,y */
        draw_sprite(g.x, g.y);
        
        /* show drawn page */
        gsetpage(PG_DISPLAY,g.page);

        /* set next page as drawing page */
        g.page = g.page ? 0 : 1;
        gsetpage(PG_WRITE,g.page);

        /* delete previous lines? */
        if (has_prev_state) {
            /* setting color to CO_BACK will delete everything
               drawn previously. */
            gsetcolor(CO_BACK);
            /* and erase sprite using previous coordinates */
            draw_sprite(g.prevx, g.prevy);
        } else 
            has_prev_state=true;

        /* store prev. state  */
        g.prevx=g.x; g.prevy=g.y;
        
        /* your function to calculate next sprite position
           and set g.x and g.y */
        calc_next_sprite_position(&g);

        /* game over? */
        if (end_of_game()) break;
    }
}

Retro Game Programming Tips, Part 1: The Game Clock