With display and sound sorted, we can now focus on the core of the interpreter!

Event loop

Let’s think about what our interpreter needs to do.

  • initialise anything SDL2-related
  • initialise the interpreter’s state
  • load the ROM into memory
  • loop forever and:
    • decode the operation
    • update the state
    • refresh the display if required
    • take user input if any

State

State is a bit of an overloaded term - as per the specs it encompasses the following:

  • memory
    • 4096 bytes
  • registers
    • 15 “general” registers and 1 special one - all 8 bits
    • the address register, 12 bits long
    • a program counter (to know where we are in the ROM), 8 bits
    • a stack pointer (to know where we are in the call stack), 8 bits
  • stack
    • 48 bytes (for 12 levels of nestings)
  • timers
    • one for sound
    • one for delay
  • input
    • 16 keys

In order to make our lives easier, and because we’re not exactly constrained from a memory perspective, we can round those up in multiples of 8 bits. If we had to define a state struct, it could look something like this:

typedef struct {
    uint8_t registers[16];
    uint16_t i;
    uint8_t pc;
    uint8_t sp;
    uint8_t stack[12];
    uint8_t delay_timer;
    uint8_t sound_timer;
    bool input[16];
} state;

We’re leaving memory out it for now - the idea being that we can “apply” a state to the memory to update it. This should help in testing things. We also need to think about the pixels representing the 64x32 screen so we may need to revisit this.

Stubs & source layout

Layout

Our project will be structured as follows:

❯ tree -P "*.c|*.h|*.txt" -I "build|Testing"
.
├── CMakeLists.txt
├── conanfile.txt
├── src
│   ├── main.c
│   ├── mylib.c
│   └── mylib.h
└── test
    └── test_a.c

main.c will contain the event loop, but pretty much everything else will be sorted in a separate “library” file. This isn’t strictly necessary but will help in testing things out. As this grows, we might split it even further but for now this should help us move things along nicely. Constants and the like will be defined in the header file mylib.h.

Stubs

The outline looks like this:

int main(int argc, char *argv[])
{
    processOptions();
    initialiseSDL2();
    initialiseInterpreterState();
    loadROM();
    while (running)
    {
        processOp();
        if (needToDraw)
        {
            updateScreen();
        }
        if (hasUserInput())
        {
            processEvent();
        }
    }
  return 0;
}

Now let’s fill this in!

Parameters

Parameters to our interpreter will be captured with the getopt library - nothing fancy, single-letter options only for now (there’s getopt_long but we’ll revisit this later if required). The two options I can think of at the outset are the scaling factor (you’ll need a magnifying glass to look at 64x32 on a FHD display - let alone something with a higher resolution) and a path to the ROM we want to load. One thing we’ll likely need down the line is a way to control the clock speed of the interpreter but we’ll kick that can down the road.

    int scale = 1;
    char *romFilename;

    int c;
    while ((c = getopt(argc, argv, "s:r:")) != -1)
    {
        switch (c)
        {
        case 's':
            scale = atoi(optarg);
            break;
        case 'r':
            romFilename = optarg;
            break;
        case '?':
            fprintf(stderr, "Scale (-s) requires an integer > 0 and ROM (-r) a path to the ROM");
            return 1;
        default:
            abort();
        }
    }

Nothing fancy here - we default scaling to 1 but don’t take any for the ROM path.

Initialising SDL2

The main bit of interest here is the scaling. We’ll still be drawing on a canvas of size 64x32 but we tell SDL2 to scale this up in integer increment (we don’t have to, but it works nicely). So for a scale of say 4, our window will actually be 256x128 and each internal “pixel” will be a 4x4 square on screen - but that’s entirely transparent to us.

    SDL_Init(SDL_INIT_EVERYTHING);

    SDL_Window *window = SDL_CreateWindow("CHIP8 Display",
                                          SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH * scale, SCREEN_HEIGHT * scale, SDL_WINDOW_RESIZABLE);
    SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE);
    SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STATIC, SCREEN_WIDTH, SCREEN_HEIGHT);
    SDL_RenderSetScale(renderer, SCALE, scale);

The SCREEN_WIDTH and SCREEN_HEIGHT contants are defined as 64 and 32 respectively in mylib.h.

One thing to note is how we use SDL_RENDERER_SOFTWARE. This means that the VSYNC timer (the refersh rate of the display, usually something like 60Hz - or 60 times per second) won’t be available to us. Not a deal breaker, but it does mean we’ll need to handle this ourselves somehow. Again, we’ll kick that can down the road.

Interpreter State

Here we create our memory structure and our state. We also create a 64x32 array representing pixels! Now the display is technically monochrome - so we could do with an array of booleans representing whether a pixel is “on” (white) or “off” (black). But SDL2 doesn’t quite work that way - the easiest representation is to have each pixel represented in an RGBA format where each value is a byte - so 32bit total. It also means we get to control the colour of what “on” and “off” mean.

    uint8_t memory[MEM_SIZE];
    memset(memory, 0x0, MEM_SIZE*sizeof(uint8_t));
    State state = {.draw = true};
    uint32_t pixels[SCREEN_WIDTH * SCREEN_HEIGHT];

MEM_SIZE is a constant we define in mylib.h to be 4096. That should be sufficient to load the ROM and any internal data structure we require. We do a memset to clear up any data - this is particularly important because memory contains the “state” of each pixel.

Loading the ROM

The first thing we need is, well, a ROM. We can get a test ROM (which is a ROM that essentially tries to cover as many op codes as possible but in a way that’s easy to understand and doesn’t contain convoluted logic) from corax89. Let’s see how it looks:

❯ wget --quiet https://github.com/corax89/chip8-test-rom/raw/master/test_opcode.ch8
❯ xxd -g 2 test_opcode.ch8 | head
00000000: 124e eaac aaea ceaa aaae e0a0 a0e0 c040  .N.............@
00000010: 40e0 e020 c0e0 e060 20e0 a0e0 2020 6040  @.. ...` ...  `@
00000020: 2040 e080 e0e0 e020 2020 e0e0 a0e0 e0e0   @.....   ......
00000030: 20e0 40a0 e0a0 e0c0 80e0 e080 c080 a040   .@............@
00000040: a0a0 a202 dab4 00ee a202 dab4 13dc 6801  ..............h.
00000050: 6905 6a0a 6b01 652a 662b a216 d8b4 a23e  i.j.k.e*f+.....>
00000060: d9b4 a202 362b a206 dab4 6b06 a21a d8b4  ....6+....k.....
00000070: a23e d9b4 a206 452a a202 dab4 6b0b a21e  .>....E*....k...
00000080: d8b4 a23e d9b4 a206 5560 a202 dab4 6b10  ...>....U`....k.
00000090: a226 d8b4 a23e d9b4 a206 76ff 462a a202  .&...>....v.F*..

We’re grouping data in size of 2 bytes because that’s the size of every operation for CHIP8.

The first operation might look a bit odd - 124e is telling us 1 (jump) to address 0x24e - but the ROM doesn’t go that far:

❯ xxd -g 2 test_opcode.ch8 | tail -2
000001c0: a202 3001 a206 3103 a206 3207 a206 dab4  ..0...1...2.....
000001d0: 6b1a a20e d8b4 a23e d9b4 1248 13dc       k......>...H..

One thing to remember is that the ROM is loaded at address 0x200 and jumps are to absolute memory locations, not base + offset. We can ask xxd to add 0x200 to every address using the -o flag:

❯ xxd -g 2 -o 0x200 test_opcode.ch8 | head -6
00000200: 124e eaac aaea ceaa aaae e0a0 a0e0 c040  .N.............@
00000210: 40e0 e020 c0e0 e060 20e0 a0e0 2020 6040  @.. ...` ...  `@
00000220: 2040 e080 e0e0 e020 2020 e0e0 a0e0 e0e0   @.....   ......
00000230: 20e0 40a0 e0a0 e0c0 80e0 e080 c080 a040   .@............@
00000240: a0a0 a202 dab4 00ee a202 dab4 13dc 6801  ..............h.
00000250: 6905 6a0a 6b01 652a 662b a216 d8b4 a23e  i.j.k.e*f+.....>

Looking at 0x24e we 6801 - the leading 6 is the “set register to value” instruction. We see a couple of registers getting set before a call to a216, which sets the I (memory) register to 216 before calling dab4 - d for drawing a sprite.

So far so good! Let’s load this file in memory then:

void loadROM(char *fileName, uint8_t memory[])
{
    FILE *fp;
    fp = fopen(fileName, "rb");
    int bytesRead = fread(memory + ROM_OFFSET, sizeof(uint8_t), MAX_ROM_SIZE, fp);
    SDL_Log("Read %d bytes from %s", bytesRead, fileName);
    int numOpcodesToPrint = 8;
    SDL_Log("The first %d opcodes are:", numOpcodesToPrint);
    int i;
    for (i = 0; i < numOpcodesToPrint; i++) {
        SDL_Log("Opcode at %x: %x%x", i*2, memory[ROM_OFFSET+i*2], memory[ROM_OFFSET+i*2+1]);
    }
}

Note that for MAX_ROM_SIZE, this will be 0xea0-0x200 - the upper bytes being reserved for display refresh, call stacks and other bits.

We also add a bit of output to ensure we’re reading this right:

❯ ./bin/fish8 -s 40 -r /tmp/test_opcode.ch8
INFO: Read 478 bytes from /tmp/test_opcode.ch8
INFO: The first 8 opcodes are:
INFO: Opcode at 0: 124e
INFO: Opcode at 2: eaac
INFO: Opcode at 4: aaea
INFO: Opcode at 6: ceaa
INFO: Opcode at 8: aaae
INFO: Opcode at a: e0a0
INFO: Opcode at c: a0e0
INFO: Opcode at e: c040
INFO: Escape pressed

Success!

Loop

This is where we’ll be processing every instruction and user input:

    while (!state.quit)
    {
        processOp(&state, memory);
        if (state.draw)
        {
            updateScreen(renderer, texture, memory, pixels);
            state.draw = false;
        }
        if (SDL_PollEvent(&event))
        {
            processEvent(&state, &event);
        }
    }

processOp

This where the meat of the interpreter will be (and subsequent posts!) - it’s a no-op for now.

updateScreen

We will call SDL_UpdateTexture by copying over the “display pixels” from the interpreter’s memory:

void updateScreen(SDL_Renderer *renderer, SDL_Texture *texture, uint8_t memory[], uint32_t pixels[])
{
    // assume we're filling by row
    uint8_t values;
    int pixelIndex = 0;
    uint16_t offset = MEM_DISPLAY_START;
    for (int row = 0; row < SCREEN_HEIGHT; row++)
    {
        // our memory unit is a byte - so each block of 8 bits represents 8 pixels
        for (int colGroup = 0; colGroup < SCREEN_WIDTH / 8; colGroup++)
        {
            values = memory[offset];
            // we now bit-shift to get the state of each pixel
            for (int shift = 7; shift >= 0; shift--)
            {
                pixels[pixelIndex] = ((values >> shift) & 0x1) ? PIXEL_ON : PIXEL_OFF;
                pixelIndex++;
            }
            offset += 1;
        }
    }
    SDL_UpdateTexture(texture, NULL, pixels, SCREEN_WIDTH * sizeof(uint32_t));
    SDL_RenderClear(renderer);
    SDL_RenderCopy(renderer, texture, NULL, NULL);
    SDL_RenderPresent(renderer);
}

Internally, memory holds the state of each pixel at 0xf00-0xfff. That’s 256 bytes, or said otherwise, 2048 bits (64x32) - one for each pixel. We need to convert each bit to an actual pixel value. For instance if we had 0xaa (which is 10101010 in binary), we’d expect an on/off pattern. I hope I’m not getting the bit ordering wrong but assume that the Most Significant Bit at the first address represents the pixel at 0,0, followed by 0,1 etc… - so we fill the top row first before moving to the next.

To validate this, I added memset(memory+MEM_DISPLAY_START, 0xaa, 256*sizeof(uint8_t)); which sure enough, toggles every other pixel:

display

Yay!

processEvent

ADDENDUM (20211210): I ended up using a slightly different approach in the end (there’s a cool datastructure provided by SDL we can use instead) - see part 7 for details

SDL_PollEvent is a non-blocking function that will return 0 if there are no events. If we do get one, we’ll update state.input accordingly to toggle which keys have been pressed/released. One thing we have to deal with however is that the CHIP8 keyboard is a 4x4 grid - which isn’t exactly available on most keyboards unless you have an ortholinear one with 4 rows. So we’ll map it as such:

 1 2 3 C     1 2 3 4 (number row)
 4 5 6 D  -> Q W E R
 7 8 9 E     A S D F
 A 0 B F     Z X C V

So whenever we get a SDL_KEYDOWN event we’ll toggle state.input on, and off on SDL_KEYUP:

void processEvent(State *state, SDL_Event *event)
{
    switch (event->type)
    {
    case SDL_QUIT:
        state->quit = true;
        SDL_Log("Quit pressed");
        break;
    case SDL_KEYDOWN:
        switch (event->key.keysym.sym)
        {
        case SDLK_q:
            state->quit = true;
            SDL_Log("Escape pressed");
            break;
        case SDLK_1:
            state->input[0x1] = true;
            break;
        default:
            break;
        }
    case SDL_KEYUP:
        switch (event->key.keysym.sym)
        {
        case SDLK_1:
            state->input[0x1] = false;
            break;
        default:
            break;
        }
    default:
        break;
    }
}

We then need case statements for each of the 16 keys - that’s it.

Conclusion

It’s a lot of stubbing but now that we have the display sorted and can capture input, we can start implementing the instructions. Let the fun begin!