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:
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:
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.
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.
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.
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:
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:
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:
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:
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
:
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!