⭅ Previous (Adding instructions)

Chip-8 Control Flow and Graphics

Welcome. Last time we looked at the general process for implementing instructions on our Chip-8 emulator. With the ability to extract operands from instructions, we know what the instruction is meant to do.

And as a bonus, the bit manipulation tricks we used were also instuctions of the Chip-8. So now we know how to implement a good chunk of basic data manipulation instructions.

Now lets try to do something visual, and add graphical support to our emulator. By the end of this article, we’ll be able to run the “Chip8 Picture” test program, which draws the text “Chip8” on the screen.

Control Flow

So far, our Chip-8 system executes every instruction in order, one after the other. The Chip-8, along with most machines, as the ability to implement control flow, where the program changes the course of instructions that will be executed.

This allows programs to implement conditions (if this then do that), and loops (do this 10 times).

We’ll look at the two control flow instructions needed by the picture program. Afterwards, you should be able to adapt this to implementing the remaining control flow instructions.

Opcode 0x1NNN : Jump to NNN

This is perhaps the simplest way to change the next instruction. If we encounter an instruction with the pattern 0x1NNN (where NNN can be any value), we set PC to NNN. Our next instruction then comes from address NNN, instead of the instruction right after the jump.

And thats all that is needed for a jump instruction.

Opcode 0x3XNN : Skip next if X == NN

This instruction has two parameters, X and NN. X refers to one of the 16 registers, and NN is any two digit value.

This instruction means “if reg X is NN, skip the next instruction”. Otherwise execution continues as normal.

All chip8 instructions are 2 bytes long. So if we choose to skip, we need to add 2 to our PC register, since it tracks bytes.

And again, that is all that’s needed. With these two instructions, we’re able to implement loops. Much of graphics code is copying data from one place to another, so these are used by the test program we aim to run.

Chip-8 Graphics

And now we’re ready to start implementing graphics for the Chip-8. Chip-8 supports monochrome (on or off) graphics, and a screen resolution of 64 pixels wide by 32 pixels tall. Not particularly large, but a surprisingly large collection of software has been written within these constraints..

First we’ll add the graphic memory to our system. We’ll represent this as a 64 x 32 array of boolean values.

This is our “framebuffer”. It holds one frame of video information as it is prepared for drawing.

Drawing the display

Chip-8 has a few mechanisms for synchronizing graphics with the game logic. We won’t worry about these for now. Instead, we’ll just try to get our graphics on the screen. This will let us confirm that the logic is correct.

One approach to drawing graphics on screen would be to integrate with a 2d graphics library. Since the screen resolution supported by chip-8 is rather low, we could actually fit the entire output into your terminal’s text-based output. One character per pixel.

We’ll add an extra “draw_screen” function to our system, which will draw the current state of the screen to the terminal. To make debugging easier, we’ll call this after every instruction. This way we can watch how the framebuffer changes over time.

The only thing our draw_screen function needs to do, is go through our display row by row, and output one character for each dot or pixel in the framebuffer.

We’ll use a space for unfilled characters, and an X for filled in characters. Assuming your console has an equal-width font, the letters should imitate the grid of a display.

Here’s roughly what that looks like:

// Rust:
const DISPLAY_WIDTH : usize = 64;
const DISPLAY_HEIGHT : usize = 32;

for y in 0 .. DISPLAY_HEIGHT {
    for x in 0 .. DISPLAY_WIDTH {
        let letter = if framebuffer[y][x] {
            'X'
        } else {
            ' '
        };
        print!("{letter}");
    }
    // Print a newline at the end of the row, to advance to the next line.
    print!("\n");
}
        

If you call your drawing code with a freshly initialized framebuffer, you would expect to see all spaces. You can manually set a pixel at say (x,y) = (5,3), and make sure something appears on screen.

Now that we can display our graphic data, lets implement the instructions that allow the Chip-8 to manipulate the framebuffer.

Opcode 0x00E0 : Clear the screen.

This instruction just sets all the pixels in the framebuffer to false. While this is the initial state of the framebuffer, most programs call this at startup anyway. We can essentially reuse our draw_screen code. Instead of printing to the terminal, we’ll set the framebuffer element to false.

Now on to the main graphics instruction:

Opcode 0xDXYN : Draw a sprite at coordinate VX, VY, N pixels tall

This cleverly selected “D” hex opcode is for drawing a sprite. A sprite is essentially a small 2d graphic that we’ll copy from memory into the framebuffer.

The first parameters are for positioning. Since a single hex digit can only hold 0 - 15, the X and Y tell us which registers are used for drawing. So 0xD128 would mean: use register 1 to find the X position, and register 2 for the Y position. Since registers are each a byte, this allows us to select any position on screen.

Sprites are always 8 pixels wide, but can be arbitrary height (1 through 15). This is because the sprite information is packed into memory. Each pixel takes up only 1 bit, so one byte/8 bits holds a full row of sprite data.

Consider this triangle we’ll try to draw:

       X     0b00000001
      XX     0b00000011
     XXX     0b00000111
    XXXX     0b00001111
   XXXXX  => 0b00011111
  XXXXXX     0b00111111
 XXXXXXX     0b01111111
XXXXXXXX     0b11111111

The most signficant bit from the sprite will be drawn into the left most pixel in the row. So the binary representation mirrors how things end up on the screen.

Now we know what the graphic data looks like, and where to put it. But where does it come from? How do we know which sprite is to be drawn? The draw command will draw the sprite which starts at memory address I.

I is a special register which is manipulated by some other instructions. It is used primarily for operations which need to read from memory. It is a 16 bit register, unlike the other 15 “numbered registers” which are only 8 bits.

Sprite rendering logic

Now we know all the pieces required to implement rendering logic. There are a few more quirks in how this instruction works:

Without further ado, our rendering code needs to:

  1. Find the registers which specify screen x,y positions
  2. Find the N value which sets the sprite height
  3. Find the I value, which tells us where sprite data starts
  4. For each row in the sprite:
  5. read the byte, and copy the bits into memory
  6. Optionally set register F (aka VF) if a 1->0 flip occurred.
let mut flag = self.registers[0xF];
let x = (self.registers[x] % DISPLAY_WIDTH) as usize;
let y = (self.registers[y] % DISPLAY_HEIGHT) as usize;

// Any 1 Sprite bits toggle the color of the display
for yi in 0 .. n as usize {
    let mut v = self.mem[self.reg_i as usize + yi as usize];
    let y = y + yi;
    for xi in 0 .. 8 {
        let x = x + xi;
        let bit = v & (1<<(7-xi));
        let dest = (y+yi) * DISPLAY_WIDTH + (x+xi);
        if dest < 0 || dest >= self.framebuffer.len() {
            continue;
        }
        self.framebuffer[y][x] ^= (bit != 0);
        if self.framebuffer[y][x] == false
            && bit == 1 {
                flag = 1;
        }
        if x == DISPLAY_WIDTH-1 {
            // clip x
            break;
        }
    }
    if y == DISPLAY_HEIGHT-1 {
        // clip y
        break;
    }
}
self.registers[0xF] = flag;

Testing Chip-8 Graphics

Now that we have all our code written, lets test it. There are a large number of test chip-8 programs compiled in this repository by loktar00 on github. We’ll use the “Chip8 Picture”, which uses only features we’ve addressed so far.

If all goes well, you should have this image on your screen after running the program:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XX                                                            XX
XX                                                            XX
XX                                                            XX
XX                                                            XX
XX                                                            XX
XX                                                            XX
XX         XXXXXXXX  X      X  X  XXXXXXXX  XXXXXXXX          XX
XX         X         X      X  X  X      X  X      X          XX
XX         X         X      X  X  X      X  X      X          XX
XX         X         X      X  X  X      X  X      X          XX
XX         X         X      X  X  X      X  X      X          XX
XX         X         X      X  X  X      X  X      X          XX
XX         X         X      X  X  X      X  X      X          XX
XX         X         XXXXXXXX  X  XXXXXXXX  XXXXXXXX          XX
XX         X         X      X  X  X         X      X          XX
XX         X         X      X  X  X         X      X          XX
XX         X         X      X  X  X         X      X          XX
XX         X         X      X  X  X         X      X          XX
XX         X         X      X  X  X         X      X          XX
XX         X         X      X  X  X         X      X          XX
XX         XXXXXXXX  X      X  X  X         XXXXXXXX          XX
XX                                                            XX
XX                                                            XX
XX                                                            XX
XX                                                            XX
XX                                                            XX
XX                                                            XX
XX                                                            XX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Conclusion

Now we’ve addressed graphics and conditional logic. There are a few other conditional instructions we didn’t mention specifically here. They’re quite similar to the one we did cover, though, and should be easy to implement on your own.

Newly supported instructions:

Type Count
Display 2
Mem (aka I register) 5
Control flow 4 total - 2 = 2

There are 2 control flow operations with a bit of nuance, which we’ll cover in the future. This brings our emulator from 16/35 supported instructions, up to 25/35.

All we have left is recursion, sound, input, and timers. What are you looking forward to most? Send me an email or message on socials.

Thanks for reading!

⭅ Previous (Adding instructions)

We publish about 1 post a week discussing emulation and retro systems. Join our email list to get notified when a new post is available. You can unsubscribe at any time.