screenshot

I’ve always been interested in emulators and writing an emulator has been in my bucket list for a long time.

I think they are a great exercise and the experience you get from them is invaluable.

Introduction to CHIP-8

So I decided to write an “emulator” for CHIP-8 programs.

In fact it should be called an “interpreter” because the CHIP-8 was never actually a real hardware, it was more like a small programming language for creating games that some old PCs supported running. So we aren’t really emulating any hardware, very sad.

However the good news is CHIP-8 feels quite close to how an emulator would be, it introduces you to the idea of interpreting the CPU opcodes, managing memory and displaying graphics.

I was planning to create a gameboy emulator at first but got overwhelmed with the technical manuals and I don’t even have any experience in using OpenGL so everything hit hard, from the memory maps and the graphics. CHIP-8 gives a nice practice so you truly understand the basics of creating an emulator.

The Language

I decided to use the Go programming language for this task because I wanted to use a language that I’d enjoy and I also really wanted to test the capabilities of another language besides C/C++ for graphical applications.

Just a little before this, I gave GLFW a try, which gives you the bare minimum you need to get started with building OpenGL programs like creating the window and attaching an OpenGL context to it and handling input.

Options like SDL are also solid choices in bigger projects, they can even abstract away OpenGL from us but my goal was also to gain experience in OpenGL so GLFW felt better.

So glfw is a C library and I tested it briefly but got bored because I didn’t know what to build then.

When this idea came up and that I wanted to use Go instead I found go-gl/glfw, a Go bindings to the GLFW library. Perfect!

I also needed a wrapper for the actual OpenGL functions luckily the same organization also offers go-gl/gl which includes bindings for all OpenGL versions.

However I found that repository to be overkill, it includes bindings for all versions so even if I don’t use them, by declaring it as a dependency in my project it has to download like 33 MB to fetch all the code. That’s a little too much for source code.

Besides all versions also include ALL extensions like those vendor-specific extensions and what not so there were too many functions and build times suffered, it took way too long to build the first program.

I found it unacceptable but luckily I found out it was generated with glow which is also by the same organization, it generates the OpenGL bindings with settings you choose, so I dived right in to get my own small and minimal OpenGL bindings with just what I need.

After using glow I created my package at ravener/go-gl which contains OpenGL 2.1 functions only with no extensions at all.

I needed pure OpenGL to learn with, I’m not in the position to be analyzing vendor-specific extensions and their uses so this is all I needed.

Additionally since I’m stuck on an old laptop for the moment with an integrated graphics card that only supports up to OpenGL 2.1 I decided to target that version.

The results was great and I’m fully satisfied with the generated bindings. Just about 300 KB of source code and short build times, I was ready to get going.

And with this, my dependencies were solved and I could start focusing on the task now.

CHIP-8 Specification

CHIP-8 consists of a 4096 bytes of memory (4 KiB) with a CPU that interprets 16-bit instructions.

You can find a full table of instructions here

The CPU consists of:

  • 16 8-bit registers labeled from V0 to VF.
  • VF register is sometimes used as a flag like for add with carry.
  • A program counter PC.
  • A stack for calling subroutines with its corresponding stack pointer.
  • A delay timer that decrements at 60Hz.
  • A sound timer that decrements at 60Hz and plays a beep sound as long as it’s not zero.
  • A 12-bit address register used to address memory.

The memory consists of:

  • 4096 bytes (4 KiB) of addressable memory.
  • ROMs are loaded at 0x200 and starts executing there, the addresses before it is reserved for the interpreter.
  • A fontset is stored somewhere inside the reserved memory area.
  • Instructions in memory are in Big Endian order.

The display consists of a 64x32 screen with its origin at the top-left and the coordinates can wrap around the screen (i.e addressing the 65th pixel from the horizontal axis is actually the 1st pixel.)

You draw on the display with rows of 8 pixels of up to the height of 15, these 8 pixel rows are just a byte in memory with each bit being used to represent the state of the pixel.

We will revise some of these details when I show you some code.

The full code can be found in my github at ravener/chip8 so if you are someone who can learn quickly from the full code just jump in with those technical details in mind.

The Memory

I started by implementing a simple way to represent memory and allow the CPU to access it.

package chip8

type Memory struct {
	RAM []byte
}

func NewMemory() *Memory {
	memory := new(Memory)
	memory.RAM = make([]byte, 4096)
	// Load fontset.
	copy(memory.RAM, Fontset[:])
	return memory
}

// Load a game ROM in the appropriate memory location.
func (m *Memory) LoadROM(rom []byte) {
	// Load ROM starting at address 0x200
	copy(m.RAM[0x200:], rom)
}

// Read a big-endian 16-bit value from the given address
func (m *Memory) ReadShort(address uint16) uint16 {
	return uint16(m.RAM[address])<<8 | uint16(m.RAM[address+1])
}

Very simple, just a slice of bytes, 4096 bytes long, a helper function to easily copy the ROM bytes into the appropriate address, a helper function that reads a 16-bit value for reading instructions.

I also loaded the fontset at the start of the RAM. I’ll get to the fontset later when we get to the graphics.

The CPU

I started by implementing a CPU that can execute instructions and worry about graphics last.

I started with a struct like the following:

type CPU struct {
	PC         uint16      // program counter
	V          [16]uint8   // registers
	I          uint16      // index register
	SP         uint8       // stack pointer
	stack      [16]uint16  // call stack
	Memory     *Memory     // RAM
	DelayTimer uint8       // delay timer
	SoundTimer uint8       // sound timer
	Display    *image.RGBA // display
	Draw       bool        // draw flag
	Keys       Keys        // input state
}

Nothing too special here, it holds everything I mentioned before.

Additionally there is a draw flag so the frontend will know when the display changes and has to redraw the graphics.

A clever trick here is that I used Go’s image.RGBA type here to represent the display, this will allow us to treat all the frames as just images and toggling real image pixels. This will allow us to leverage Go’s image encoders to easily save screenshots and preview frames before even writing the OpenGL frontend.

Go’s image.At(x, y)/image.Set(x, y, color) will also prove useful to simplify the math of accessing pixels.

Then I created a fetch function that returns the current opcode and advances the program counter forward.

func (cpu *CPU) fetch() uint16 {
	instruction := cpu.Memory.ReadShort(cpu.PC)
	// Increment program counter to point to the next instruction.
	cpu.PC += 2
	return instruction
}

A method to clear the display:

func (cpu *CPU) ClearDisplay() {
	for x := 0; x < 64; x++ {
		for y := 0; y < 32; y++ {
			// Set to a black pixel.
			cpu.Display.SetRGBA(x, y, color.RGBA{A: 255})
		}
	}
}

The image starts at completely zero state, that also includes the alpha transparency value so later on if we take a screenshot the non-set pixels will all be fully transparent, not what we want. So this method is also called at the initialization so we can get a fully black screen to render to.

Additionally there is also an opcode that requests to clear the screen and we can simply call this function.

Next is the main loop for interpreting the CPU:

It starts at something like this:

func (cpu *CPU) Execute() {
	op := cpu.fetch()

	switch {
	case op == 0x00E0: // 00E0: Clear the screen
		cpu.ClearDisplay()
		cpu.Draw = true
	case ...:
	    // ...
	default:
	    log.Fatalf("Unhandled instruction: 0x%X\n", op)
	}
}

Whenever this method is called a cycle of the CPU is executed, we start by fetching the opcode which also increments the program counter.

The tricky part now is that opcodes are 16-bits and some bits contain dynamic values so we cannot do switch op {} I instead opted for a switch {} which in Go is simply an implicit switch (true) {} and decided to put the full condition inside the case statements.

So let’s talk about the instructions. Usually there is the first nibble that contains something to compare to decide which instruction to run and other tutorials would normally create their switch statement based on the first nibble, the problem is there will be further logic involved later on and another switch case must be made, I really wanted to avoid nested switch cases so I opted for this method so I can specify the full condition of matching each opcodes.

So what are these dynamic instructions? Let’s see an example:

Let’s look at 7XNN this instruction is described as “Add NN to VX”, here X is an index to a register inside our V array, which contains all our 16 registers, remember?

This is specified in hexadecimal format so an example opcode would be 0x7282 this would mean add 0x82 into V2

So some bitwise operations are required to extract what we need. We can use the & operator to filter and get the bits we need first so we can compare if this is the opcode we need:

case op&0xF000 == 0x7000:

The & over there tells us to get the first nibble and fill the rest with zeroes so we can now directly compare to 0x7000 ignoring the other dynamic values. With this the comparison is done so we can start executing the instruction when it is the right opcode.

Now for the actual opcode:

case op&0xF000 == 0x7000: // 7XNN: Add NN to VX
	cpu.V[op>>8&0xF] += uint8(op & 0xFF)

This is how I started initially, you can see I access the V register associated with X which is the second nibble, to reach it I first have to shift the current opcode by 8 bits (1 byte, 2 nibbles), in hex each digit is a nibble so that looks like we are throwing away the NN value out and we can use the & operator again to get the lowest nibble which is now our X value. This way we access the right VX register.

We then add (+=) the value of NN which is the two last nibbles so another bit mask to get those two nibbles only.

This is the tedious process that you now have to go through for all opcodes. Read their descriptions and implement them.

There is one thing we can do to make it cleaner, the pattern that the dynamic values appear is pretty much constant across instructions, as in the X value in there is always the second nibble, X never comes at the 3rd nibble or anything so we can actually declare those values once before the switch and use it for any instructions that use them:

X := op >> 8 & 0xF
Y := op >> 4 & 0xF
NN := uint8(op & 0xFF)
NNN := op & 0x0FFF

Some of the common values we will need.

Now those will point to invalid values when the instruction does not use the X field and vice versa but then we won’t access them either so it’s fine. It may also slightly be a waste to decode all those when it might not be accessed in this cycle at all but don’t underestimate your processor, these bitwise operations are nothing and the CHIP-8 interpreter is nothing so I’d advise to prioritize clean code over worrying about any performance bottlenecks at all for this project, you are only trying to set foot into how emulators work rather than creating some highly optimized app.

Now the above instruction can be rewritten as:

case op&0xF000 == 0x7000: // 7XNN: Add NN to VX
	cpu.V[X] += NN

Much cleaner, the comments I added to describe the instruction now matches the code closely without seeing too much bitwise magic.

Now this goes on until you have all instructions ready, I won’t talk about every instruction here, that’s left as an exercise for you, pull up an opcode reference and start writing them or you can read my source code for hints.

I will discuss a few instructions that need special care like the graphics.

The Graphics

Graphics in the CHIP-8 is done by drawing so-called “sprites” on a 64x32 display.

A sprite is defined by a row of 8 pixels. This row is one byte long (i.e 8-bits) and each bit says whether that pixel is on.

We can start by analyzing the fontset, which is also needed:

I put this in its own file which is referenced by the memory module I showed earlier.

package chip8

// The CHIP-8 fontset.
var Fontset = [80]byte{
	0xF0, 0x90, 0x90, 0x90, 0xF0, // 0
	0x20, 0x60, 0x20, 0x20, 0x70, // 1
	0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2
	0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3
	0x90, 0x90, 0xF0, 0x10, 0x10, // 4
	0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5
	0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6
	0xF0, 0x10, 0x20, 0x40, 0x40, // 7
	0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8
	0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9
	0xF0, 0x90, 0xF0, 0x90, 0x90, // A
	0xE0, 0x90, 0xE0, 0x90, 0xE0, // B
	0xF0, 0x80, 0x80, 0x80, 0xF0, // C
	0xE0, 0x90, 0x90, 0x90, 0xE0, // D
	0xF0, 0x80, 0xF0, 0x80, 0xF0, // E
	0xF0, 0x80, 0xF0, 0x80, 0x80, // F
}

This is the default fontset containing the sprite data to draw numbers 0-9 and letters A-F, basically the hexadecimal set.

Let’s take a look at the first element:

0xF0, 0x90, 0x90, 0x90, 0xF0, // 0

0xF0 = 11110000
0x90 = 10010000
0x90 = 10010000
0x90 = 10010000
0xF0 = 11110000

Each byte is a row so we arranged it in rows above to get the pixels. The 1s are the white pixels shown on the screen. To get a better look let’s remove the zeroes (the off pixels)

1111
1  1
1  1
1  1
1111

There we go, that’s a zero right there, right?

So how are these fonts accessed? There is an opcode FX29 that looks at the value of VX and grabs the corresponding value’s font data to be stored in I which is used to address memory.

case op&0xF0FF == 0xF029: // FX29: Set I to sprite data for VX
	cpu.I = uint16(cpu.V[X] * 5)

Since each sprite was 5 rows long we multiply by 5 to get the right offset.

This now makes I point to the correct memory address for the font’s sprite. Remember we loaded the fontset at the beginning of the memory so this points correctly. If say you loaded it at a higher point in the reserved memory, you’d make sure to add that value in the above instruction so the offset is correct.

That’s it for the builtin fonts, so how do we actually render this graphics? There is the DXYN instruction to draw sprites at the coordinates (VX, VY) that is N rows tall.

Since N is only a nibble long (i.e 4-bits) it’s limited up to 15 rows.

The sprite data should be pulled from the memory at the address set by I so if the sprite is 3 rows tall (N = 3) then we can find those rows at the addresses I, I+1 and I+2 inside the memory.

Then for each row we have to check each bit for the pixels. If it’s a 1 the pixel is “toggled” so we have to check the current display pixel’s value as well. Additionally if the current pixel was on then before turning it off we also set the VF flag to signal a pixel “collision” this is basically how collision detection is done in CHIP-8 games.

This pixel toggling is also known as XOR which toggles bits:

Current Display: 10011001 (what is on the screen)
Current Row:     10010010 (what the sprite demands)
Output:          00001011 (what will be drawn)

Since we are dealing with a higher level display structure (a whole RGBA image instead of raw data) we can’t directly use the XOR operator itself here but the concept is the same, we need to toggle the pixels that are set in the sprite’s row.

Let’s talk about the coordinates for a second, they start at the top-left origin which matches Go’s image.RGBA so it simplifies our math. Additionally coordinates are supposed to wrap around, i.e if we ask for the 65th pixel in the horizontal axis it points at the first pixel. This is simply a matter of using the modulo operator:

// The coordinates are supposed to wrap around the screen.
// We do this by taking the modulo of the point by the axis length.
if x > 63 {
	x %= 64
}

// Do the same for the y-axis.
if y > 31 {
	y %= 32
}

Remeber the coordinates start at 0 so we use 63/31 instead of 64/32

Ok, finally here’s my complete implementation of the DXYN instruction, are you ready?

case op&0xF000 == 0xD000: // DXYN: Draw a sprite
	// Draw a sprite at position VX, VY with N bytes of sprite data starting at
	// the address stored in I
	// Set VF to 01 if any set pixels are changed to unset, and 00 otherwise.
	x := int(cpu.V[X])
	y := int(cpu.V[Y])

	// The coordinates are supposed to wrap around the screen.
	// We do this by taking the modulo of the point by the axis length.
	if x > 63 {
		x %= 64
	}

	// Do the same for the y-axis.
	if y > 31 {
		y %= 32
	}

	height := int(op & 0x000F)
	// Reset the VF flag.
	cpu.V[0xF] = 0
	// A sprite consists of 8 pixels per-row
	// of upto 15 rows tall.
	// each row is just a byte with pixel data in each bit.
	for i := 0; i < height; i++ {
		row := cpu.Memory.RAM[cpu.I+uint16(i)]
		bit := 0

		for bit < 8 {
			// If this bit is on.
			if row&0x80 != 0 {
				// Grab the current pixel that is on the display.
				r, g, b, a := cpu.Display.At(x+bit, y+i).RGBA()

				// If the current pixel is on.
				if r == 0xFFFF && g == 0xFFFF && b == 0xFFFF && a == 0xFFFF {
					// Set the VF flag to indicate this pixel collision.
					cpu.V[0xF] = 1
					// Turn off the pixel.
					cpu.Display.SetRGBA(x+bit, y+i, color.RGBA{A: 255})
				} else {
					// Turn on the pixel.
					cpu.Display.SetRGBA(x+bit, y+i, color.RGBA{R: 255, G: 255, B: 255, A: 255})
				}
			}

			// Check the next bit.
			bit++
			row <<= 1
		}
	}
	// Set the draw flag to notify that the display has changed and must be redrawn.
	cpu.Draw = true

At this point, assuming we implemented enough CPU instructions you can just use Go’s image encoders to save the image to a file and preview some frames to test your graphics code before getting our hands dirty with OpenGL later.

The Timers

CHIP-8 contains two timers, a delay timer and a sound timer.

These timers tick at 60Hz meaning they keep decrementing 60 times per second until they are zero. Assuming a rate of 60 FPS this means we have to subtract one from them every frame.

The delay timer can be set and get which the game uses for its own timing purposes while the sound timer can only be set and as long as it’s not zero a beeping sound is to be played.

I made a method for them to keep things clean:

func (cpu *CPU) UpdateSoundTimer() {
	if cpu.SoundTimer > 0 {
		cpu.SoundTimer--
		// TODO: Audio
		if cpu.SoundTimer == 0 {
			log.Println("BEEP")
		}
	}
}

func (cpu *CPU) UpdateDelayTimer() {
	if cpu.DelayTimer > 0 {
		cpu.DelayTimer--
	}
}

// Update the timers. This must be called at 60 Hz
func (cpu *CPU) UpdateTimers() {
	cpu.UpdateDelayTimer()
	cpu.UpdateSoundTimer()
}

I did not get into adding audio yet so I just put a placeholder beep message to be printed in the console so we know audio is supposed to be playing here.

The corresponding instructions to set these timers are also really simple so I don’t need to talk about them.

The Input

Now the CPU also needs to recieve some user input.

The CHIP-8 contains a keypad that looks like:

+-+-+-+-+
|1|2|3|C|
+-+-+-+-+
|4|5|6|D|
+-+-+-+-+
|7|8|9|E|
+-+-+-+-+
|A|0|B|F|
+-+-+-+-+

It contains only hexadecimal digits. So I made a Keys structure to hold the state of keys.

package chip8

type Keys [16]bool

// Check if a key is currently pressed.
func (keys *Keys) IsPressed(key uint8) bool {
	return keys[key]
}

// Set a key to pressed state.
func (keys *Keys) Press(key uint8) {
	keys[key] = true
}

// Set a key to released state.
func (keys *Keys) Release(key uint8) {
	keys[key] = false
}

Also added methods that will change the state of the keys which will be called by the frontend that handles the window and the actual input events.

Then there are some conditional instructions that skips a instruction based on whether a key is pressed or not which you can now make use of the above IsPressed method to implement, easy.

The tricky one is the FX01 which waits for an input to arrive, so we need to stop executing more instructions until an input is recieved and store that key into VX

I simply hacked my way by checking all input states and if no key is pressed, set the PC backwards by an instruction so that the next cycle executes this same instruction, hence checking keys again in a recursive manner until a key is pressed.

case op&0xF0FF == 0xF00A: // FX01: Wait for keypress and store key in VX
	recieved := false
	for key, pressed := range cpu.Keys {
		if pressed {
			cpu.V[X] = uint8(key)
			recieved = true
		}
	}

	if !recieved {
		// If we didn't recieve a keypress, go back one cycle to retry.
		cpu.PC -= 2
	}

There is probably a better way to do this but it just works and it’s simple.

The Frontend

That concludes the backend now we need to create a frontend that makes it all tick and show it to the user.

I won’t show you all the code here since it’s not too interesting but I’ll talk about some of the challenges I faced.

GLFW

I simply used the glfw library I mentioned at the beginning to create a window with an OpenGL context and the generated OpenGL bindings which I made using glow.

The important thing to know is to keep OpenGL calls to a single thread, additionally operating systems would prefer you handle input and other OS events on the main thread, especially macOS won’t allow you to do any of those on another thread.

You don’t need to know too much about threading, in fact we won’t use threads at all but as it turns out the Go runtime probably uses them internally and that can mess up our environment, the solution is simple, Go allows us to lock the OS thread and that’s what we’ll do.

import "runtime"

func init() {
    // GLFW event handling must run on the main OS thread
    runtime.LockOSThread()
}

Don’t underestimate this line of code, I forgot to do this at the start and had to go through so much pain because OpenGL kept acting weird, working for a moment and breaking after a few seconds, graphical glitches and all. It took me a while to realize I forgot to include that line of code.

OpenGL Textures

So how do we actually get the display rendering? The idea is simple we use an OpenGL texture to hold the image data and render that texture.

Textures are simply images that are stored in the GPU memory. OpenGL renders polygons/vertices that can be optionally textured by an image.

So we render a rectangle that covers the entire screen which is textured with the display pixels.

So let’s start creating the texture:

func createTexture(img *image.RGBA) uint32 {
	var texture uint32

	gl.GenTextures(1, &texture)
	gl.BindTexture(gl.TEXTURE_2D, texture)
	gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
	gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
	gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 64, 32, 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(img.Pix))

	return texture
}

First glGenTextures is used to generate a texture, the return value is an integer that is actually a pointer but it’s pointing to the GPU memory.

We then bind that texture into GL_TEXTURE_2D so we can start working on it. Then set some parameters, the sampling filter which is required or the texture won’t be sampled at all.

And finally we upload the image using glTexImage2D which takes some parameters to tell it what format the image is, in this case we are trying to upload the Go image.RGBA structure which holds RGBA values in bytes so that’s what we specify then we upload a pointer to the raw pixels array of the image.

We also need a method to update the texture, the image lives in the GPU so everytime our display changes we have to tell the GPU memory to update with the new texture data.

func updateTexture(img *image.RGBA) {
	gl.TexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 64, 32, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(img.Pix))
}

We use glTexSubImage2D which can update a portion of the texture, it’s faster than creating the texture data again with glTexImage2D, we specify the image format again and we are done.

Make sure that OpenGL textures are enabled in the first place, within your initialization code you should add this:

gl.Enable(gl.TEXTURE_2D)

with this we are done with the texture and we now should render this.

OpenGL rendering

OpenGL rendering is done through primitive shapes (quads, triangles, lines, points) which can be textured.

I decided to use legacy OpenGL functions for this project because it is much easier and this wasn’t that big of a project to care.

OpenGL 3+ started including a so-called “core profile” which if requested at context creation will disable all legacy/deprecated functions.

One of those deprecated stuff is the Quads shape, it has been removed and to draw a quad you simply have to render two triangles in a way that merges together to form a quad shape.

Another one is the immediate rendering mode, you could just call glBegin(shape) and specify vertices and glEnd() and poof you got graphics, just keep doing that every frame.

In a way it is ineffecient so it’s understandable why it’d be deprecated but nothing beats its simplicity.

The modern way is to create a Vertex Buffer Object (VBO), a Vertex Array Object (VAO) and an Element Buffer Object (EBO) with the vertices being two triangles to cover the screen and you have to write the basic vertex shader and a fragment shader to get the vertices and texture sampling across.

That just felt like too much work for me and for such a small project I decided I want to quickly experience OpenGL and get a project done so I decided to use the legacy immediate mode and man, it was so simple.

Here’s the rendering code:

gl.Begin(gl.QUADS)
gl.TexCoord2f(0, 1)
gl.Vertex2f(-1, -1)
gl.TexCoord2f(1, 1)
gl.Vertex2f(1, -1)
gl.TexCoord2f(1, 0)
gl.Vertex2f(1, 1)
gl.TexCoord2f(0, 0)
gl.Vertex2f(-1, 1)
gl.End()

Now I won’t go too deep into OpenGL here but yeah that’s it.

The point of this section was to give you the basic mindset of how OpenGL can be applied in this area because that was something that took me a while to realize and so this not a definite tutorial to OpenGL.

Timing

Another challenge is the timing. CHIP-8 CPU is supposed to run at ~500-540 Hz while the delay timer and the sound timer has to tick at 60 Hz.

I kind of cheated through this by relying on V-Sync to make my loop run at 60 FPS, then in each frame I update the delay timer and sound timer which makes them tick at 60 Hz and for the CPU you simply execute 9 cycles per frame, since 60 x 9 = 540 and that keeps our CPU at the 540 Hz rate needed.

Now this has it’s problems because V-Sync synchronizes to the monitor’s refresh rate and I assumed a 60 Hz monitor. If someone else runs it on one of those 240 Hz monitors, everything will be too fast.

This needs to be improved and there’s quite some stuff I need to learn to better understand this.

But CHIP-8 timing doesn’t need to be too accurate, it isn’t the end of the world if it runs a bit faster or slower, all I cared about it is to get this thing even working and I’m fairly satisfied with the results.

CPU Test

There is a CHIP-8 rom that can test if you have implemented all the opcodes correctly, so I ran it on my interpreter and success!

screenshot

Conclusion

So that concludes my post about writing a CHIP-8 intepreter, it isn’t perfect, I still need to add audio and fix the timing but it was fun, I learned a lot and it’s been a while since I made any new projects so that was a fun little thing to work on and add to my portfolio.

I’m happy that I went with Go instead of the usual C/C++ solutions so I can try something new and it was great.

All in all this wasn’t meant to be a perfect step-by-step tutorial, It was mainly trying to show my experience in building one and give you the basic idea about it, now go implement one with your own twist, use your favorite technologies, like if you are a web developer you can try making one with JavaScript and the Canvas Web APIs to get this on the browser or even the hot new web assembly stuff. The idea is to have fun trying out new things and I surely had my fun.