Writing a Goboy: making a readable display
October 15, 2019
Goboy has reached a major milestone – a working display!
Recently, my development process has been a bit unorthodox. My wife and I are spending this year travelling and so I only have bits of time between the many, many temple visits to program and blog. The display is the most complex single component of the Gameboy and, in my opinion, implementing it is pretty much an all or nothing job. You can’t really built it piece by piece as you can with the instruction set. Each time I worked on the display it took me a little while to rebuild my mental model of how the display works and all of its (sometimes obtuse) details. I had started work on the display while we had a solid block of time in one country. I was also working remotely at this time so progress had been pretty slow and I was worried I wouldn’t finish it before we started moving again. The thought of having to tackle this pile of half-finished, broken code on a series of night buses did not appeal.
I decided to rush to get the display working. After that I could come back and refactor/bugfix/improve at my leisure. It’s much easier to take a few minutes and refactor a single function, make naming more consistent or what have you than to try and implement a new feature. Having a working display also makes it much easier to catch regressions. To get the display working quickly I leaned heavily on this excellent series of blog posts and in particular on dmgo, a Gameboy emulator in Go. I liked this one because as well as being in Go it was well-written. I started off by pretty much copying sections of code over and fudging them together until they worked with my existing codebase. As I continued refactoring things have diverged enough that I’m comfortable saying it’s my own work but I am very indebted to these resources.
At a high level the display isn’t that complex. It follows the old CRT/raster model of rendering a series of horizontal scanlines to build up the image. Each scanline is followed by a horizontal blank interrupt to mimic moving back to the beginning of the next line. When all the scanlines are rendered we’re at the bottom right of the display (the end of the final scanline) so there is a longer vertical blank to mimic the time it takes to return to the top left to start again. The image is made up of a static background, an optional window overlay and sprites for rendering changeable elements. So to take Tetris as an example, everything is part of the background except for the currently falling piece, which is rendered as a sprite. When it reaches the bottom it won’t move again so the background is updated to include it. The devil is in the details of how the Gameboy stores the display data in memory, the timings of the various modes that it cycles through as part of the rendering process and various odd restrictions imposed by the hardware’s limitations. I won’t describe them in detail since the linked blog posts above already do a fine job.
Overall I found the display implementation to be an unenjoyable task. You need to spend a lot of time reading the specs to make sure you have a good grasp of how things work. The Gameboy stores data in odd formats and decoding them is fiddly. Bugs can be hard to track down. It’s not really that interesting as a programming challenge – you just need to follow a lot of fiddly requirements. On the other hand, I enjoyed the challenge of refactoring my working-but-messy implementation into clean, self-documenting code. I’m pretty happy with what I’ve got now and hope that it makes sense to someone who hasn’t memorised the entire display specs. To achieve this I’ve relied on a few simple techniques: using named constants, writing small and descriptive functions, separating responsibilties and using the type system to state and protect intent.
Named constants
I’ve touched on this before, but a common stumbling block to understanding other Gameboy implementations is the use of unexplained constant values. When you read:
line *= 2
dataAddress := (0x8000 + uint16(tileLocation*16)) + uint16(line)
The natural thing is wonder why line
is multiplied by two, what’s at 0x8000
and why tileLocation
is multiplied by 16. The only solution is to dig into the docs and work out what the various numbers correspond to. This means that the code can’t be understood without recourse to the documentation. Using named constants reduces the amount of implied knowledge. My aim is to use descriptive names so that a function can be understood without reference to anything else:
func getSpriteAddress(tileNum byte) uint16 {
return SpriteDataStartAddress + (uint16(tileNum) * SpriteDataSize)
}
Small, descriptive functions
The example above also demonstrates the benefits of breaking things into small functions. Instead of a long sequence of memory accesses, bit manipulations and calculations you can express the logical steps of the algorithm via the function names:
dataAddress, addressMode := gpu.bgTileDataAddress()
tileNum := gpu.fetchTileNum(startAddress, x, y)
tileLocation := getTileLocation(addressMode, dataAddress, tileNum)
The reader can follow the steps of the rendering process without having to concern themselves too much in the details of exactly how every value is computed. This is particularly helpful when naming variables and constants isn’t enough. Look at the following example:
func getColourCodeFrom(xPos, low, high byte) colourCode {
bitOffset := xPos & CharCodeMask
bitL := (low >> (7 - bitOffset)) & 0x1
bitH := (high >> (7 - bitOffset)) & 0x1
return colourCode((bitH << 1) | bitL)
}
The Gameboy stores colour code data across two bytes. The bits in one byte represent the least significant bits of eight colour codes and the bits in the other byte represent the most significant bits. This function pulls the correct pair from the two bytes and returns a colourCode
. Perhaps this is due to a failure of my imagination but I just couldn’t see how naming the numbers in this function would do much to expalin what’s happening. In my opinion it’s preferable to wrap this all away in a well-named function.
Roughly, naming constants is useful for when I want each step of the computation to be more easily understandable. Using small functions is more useful when I want to treat the computation steps as an implementation detail and help the reader to understand the code at the conceptual level. Obviously both can be used together and my current implementation uses a mix of both. I suspect that getting the balance right is more a matter of style than anything else.
Separating responsibilities
This is just basic use of interfaces. I’m using Ebiten to render the display. At some point I’m planning to compile to WebAssembly and run the emulator in the browser. For that I’ll need to render the display differently so I wanted to keep implementation-dependent stuff separate from the Gameboy internal stuff (which I have called the GPU).
The GPU holds a display interface:
type GPU struct {
cpu *CPU
display DisplayInterface
/* ... */
}
type DisplayInterface interface {
WritePixel(x, y, r, g, b byte)
}
And then the display is just a buffer than can be passed to ebiten in its run loop:
package display
type Display struct {
buffer *image.RGBA
}
func (d *Display) WritePixel(x, y, r, g, b byte) {
yIdx := int(y)*160 + int(x)
d.buffer.Pix[yIdx*4] = byte(r)
d.buffer.Pix[yIdx*4+1] = byte(g)
d.buffer.Pix[yIdx*4+2] = byte(b)
d.buffer.Pix[yIdx*4+3] = 0xff
}
// in cmd/goboy/main.go
f := func(screen *ebiten.Image) error {
for i := 0; i < CyclesPerFrame; i++ {
cycles := gameboy.Step()
gameboy.UpdateDisplay()
}
screen.ReplacePixels(display.Pixels())
return nil
}
This keeps all the buffer writing separate from the Gameboy, which naturally doesn’t need to know about that. Admittedly I don’t think I’ve got the division between the ebiten code and the display buffer quite right yet. I’m waiting until I implement the WebAssembly version to get a better idea of how to do this.
Stating and protecting intent through types
This was prompted by reading through the Uber Golang style guide. Rather than having a function signature like this:
func (gpu *GPU) fetchPixel(x, y byte) (byte, byte, byte, bool)
I created descriptive types so that the signatures were more self-documenting:
type RGB struct {
r, g, b byte
}
type pixelVisibility bool
const (
visible = true
invisible = false
)
func (gpu *GPU) fetchPixel(x, y byte) (RGB, pixelVisibility)
Giving variables sensible names goes some way to achieving the same result but I like that the function is self-documenting without having to inspect its call sites to see how the return values are used. I try to only have return types like (byte, byte)
when the return value actually is a pair of bytes. It also provides a bit of type safety as a “byte” might variously be a colour code, a tile number or a flagset. Specifying argument and return types helped me to clear up some areas where I’d given variables or functions slightly inaccurate names.
I haven’t given everything its own type. Scanlines are just bytes, for example. This is inconsistent but I felt that the way scanlines were used was straightforward enough. They aren’t new values computed from others, which is where I think descriptive types really help.
A further step was to create structs out of the raw bytes in the Gameboy memory so that I could add descriptive methods to the structs rather than constantly checking whether bits are set in flags and so on. This is a straight copy from the dmgo emulator:
type oamEntry struct {
x int16
y int16
height byte
tileNum tileNum
flags flags
}
func (e *oamEntry) behindBG() bool { return e.flags&0x80 != 0 }
func (e *oamEntry) yFlip() bool { return e.flags&0x40 != 0 }
func (e *oamEntry) xFlip() bool { return e.flags&0x20 != 0 }
func (e *oamEntry) useOBP1() bool { return e.flags&0x10 != 0 }
I thought it was a really good approach so I did something similar with the status and control registers the GPU uses. The registers are just aliases for bytes with helper methods defined on them:
type GPUControl byte
type GPUControlFlag byte
const (
LCDDisplayEnable GPUControlFlag = 0x80
/* ... */
)
func (gpu *GPU) getControl() GPUControl {
return GPUControl(gpu.cpu.getLCDC())
}
func (control GPUControl) isDisplayEnabled() bool {
return control.isSet(LCDDisplayEnable)
}
Leading to a nice usage:
if gpu.getControl().useHighWindowAddress() {
return 0x9C00
}
This abstracts all of the flag details away from the code that queries the registers. I think this pattern is worth applying throughout the codebase.
Conclusion
Overall the display implementation is in a good place. I may uncover a few more bugs as I try with more games but I feel confident that I can refactor the code without getting lost in the Gameboy’s hardware details. While it wasn’t much fun getting things working, I have enjoyed the task of refactoring the code to make it more self-documenting.
Next up are the comparatively straightforward timers, interrupts and joypad systems. Once these are in place we can start playing simple games like Tetris.