Learning Rust by building a partial Game Boy emulator.
This project is maintained by jeremyBanks
Outstanding items from yesterday:
0xCB
0x7C
bit-check.I also rewatched some of The Ultimate Game Boy Talk again, and it gave me a couple other things to address soon:
F
register, I had it as a distinct thing. Oops.0x0
in memory. It should be easy to put it there, and start processing instructions through normal memory access.My (incorrectly) big-endian byte- and bit-wise logic was duplicated across a few register accessor methods. It didn’t look great.
fn set_h_flag(&mut self, value: bool) {
self.flag_register = (self.flag_register & 0b11011111) + (if value { 0b00100000 } else { 0 });
}
While going to fix it, I decided to pull that out into a few utility functions. They initially looked like this:
fn u8s_to_u16(a: u8, b: u8, x: &mut u16) {
*x = a as u16 + ((b as u16) << 8)
}
fn u16_to_u8s(x: u16, a: &mut u8, b: &mut u8) {
*a = x as u8;
*b = (x >> 8) as u8;
}
fn u8_get_bit(x: u8, offset: u8) -> bool {
(x >> offset) & 1 == 1
}
fn u8_set_bit(x: &mut u8, offset: u8, value: bool) {
let mask = 1 << offset;
if value {
*x |= mask;
} else {
*x &= !mask;
}
}
With:
fn set_h_flag(&mut self, value: bool) {
u8_set_bit(&self.flag_register, 3, value)
}
Typing an argument as &mut u8
instead of u8
is kind-of like using a pointer type in C, but more restricted. The &
means that I am “borrowing” the value, so the compiler will ensure that I don’t hold onto that reference longer than expected and do something unsafe. Since I’m just using it immediately before ending the function (and returning the “borrow”), there’s no problem here. The mut
annotation is required because I’m going to be mutating it. Rust seems to require mut
almost everything that you have non-strictly-functional behaviour. I love it! But I’m not really striving for a functional style here yet, so I’ll probably be typing it a lot.
While implementing this, I ran across a quirky choice of Rust: functions implicitly return the last expression in their bodies, unless it’s followed by a semicolon. So omitting the semicolon at the end is the same as adding return
to the beginning. This looks clean, but is very strange and not obvious when looking at the source. It might normally bother me, but the compiler provides such precise error messages:
error[E0308]: mismatched types
--> src/main.rs:231:30
|
231 | fn z_flag(&self) -> bool {
| ______________________________^
232 | | u8_get_bit(self.flag_register, 1);
| | - help: consider removing this semicolon
233 | | }
| |_____^ expected bool, found ()
|
= note: expected type `bool`
found type `()`
An easy one-character fix.
Unfortunately (or maybe fortunately), my idea of having u16_to_u8s
write its results directly to two borrowed &mut u8
references don’t seem to be allowed. The function definition is fine, but when I go to use it, I get this error:
error[E0499]: cannot borrow `self.main_registers[..]` as mutable more than once at a time
--> src/main.rs:220:62
|
220 | u16_to_u8s(value, &mut self.main_registers[10], &mut self.main_registers[11]);
| ----------------------- ^^^^^^^^^^^^^^^^^^^^^^^- first borrow ends here
| | |
| | second mutable borrow occurs here
| first mutable borrow occurs here
I guess the borrow checker doesn’t understand that these references are to non-overlapping sections of memory, and is concerned that multiple mutable borrowings could mean that the array is modified from different places at once, unsafely. There may be some way to explain what I’m doing to the compiler more clearly, either by using some kind-of joint borrow, or a narrower one, with some kind of complicated lifetime annotation. But it’s probably a better idea to just change the method to the functional style the language encourages.
Rather than requiring multiple “out parameters”, Rust allows us to use tuples for multiple return values:
fn u16_to_u8s(x: u16) -> (u8, u8) {
(x as u8, (x >> 8) as u8)
}
And supports destructuring assignment to easily consume them:
fn set_pc(&mut self, value: u16) {
let (p, c) = u16_to_u8s(value);
self.main_registers[10] = p;
self.main_registers[11] = c;
}
With these changes finally compiling, we can run and get the next expected error:
read opcode 0x32 at 0x7
memory[HL] = A; HL -= 1
memory[0x9FFF] = 0x0
thread 'main' panicked at 'I don't know how to set address 0x9FFF.', src/main.rs:270:13
We hadn’t implemented support for the Video RAM memory range, but the initial implementation just took a few minutes, bringing us back to one of our questions from yesterday: why isn’t this code looping? It’s supposed to run 8192 times, but it’s only running once before continuing to new instructions:
Running `target/debug/zerodmg`
read opcode 0x31 at 0x0
SP = 0xFE, 0xFF
read opcode 0xAF at 0x3
A ^= A (A = 0)
read opcode 0x21 at 0x4
H, L = 0xFF, 0x9F
read opcode 0x32 at 0x7
memory[HL] = A; HL -= 1
memory[0x9FFF] = 0x0
video_ram[0x1FFF] = 0x0
read opcode 0xCB at 0x8
read opcode_2 0x7C
setting Z flag to 7th bit of H register (true)
read opcode 0x20 at 0xA
relative jump of -5 if Z flag is false (it is true)
read opcode 0x21 at 0xC
H, L = 0x26, 0xFF
read opcode 0xE at 0xF
thread 'main' panicked at 'unsupported opcode: E', src/main.rs:109:21
Answer: I missed an important part of the definition of the BIT
instruction:
0xCB7C
: set theZ
flag bit to the opposite of the 8th bit of theH
register.
I was testing the wrong bit, and and I was inverting the result. Oops! I discovered this by searching for “z80 bit instruction” and landing on this wiki page, after the talk reminded me that these instructions are based on instructions from the z80.
This gets us 128 iterations, but no more, as anticipated at the end of yesterday’s post. We’re checking H
, which we are treating as the less-significant byte out of HL
because that seems like the little-endian thing to do, but the logic wants us to be waiting for the most-significant bit of the most-significant byte to flip, when we pass below 0x8000 = 0b1000000000000000
.
This doesn’t make sense to me. I’m going to just flip the names of the individual register accessors, but keep the joint ones working the same. If this continues to work, I’ll need to make it and the other register accessors consistent and figure out how to make that coherent, but let’s just run with it for a bit first, since it gets us to our next instruction:
[...]
read opcode 0x32 at 0x7 at t=24570
memory[HL] = A; HL -= 1
memory[0x8002] = 0x0
video_ram[0x2] = 0x0
read opcode 0xCB at 0x8 at t=24571
read opcode_2 0x7C
setting Z flag to false, the opposite of 8th bit of H register
read opcode 0x20 at 0xA at t=24572
relative jump of -5 if Z flag is false (it is false)
read opcode 0x32 at 0x7 at t=24573
memory[HL] = A; HL -= 1
memory[0x8001] = 0x0
video_ram[0x1] = 0x0
read opcode 0xCB at 0x8 at t=24574
read opcode_2 0x7C
setting Z flag to false, the opposite of 8th bit of H register
read opcode 0x20 at 0xA at t=24575
relative jump of -5 if Z flag is false (it is false)
read opcode 0x32 at 0x7 at t=24576
memory[HL] = A; HL -= 1
memory[0x8000] = 0x0
video_ram[0x0] = 0x0
read opcode 0xCB at 0x8 at t=24577
read opcode_2 0x7C
setting Z flag to true, the opposite of 8th bit of H register
read opcode 0x20 at 0xA at t=24578
relative jump of -5 if Z flag is false (it is true)
read opcode 0x21 at 0xC at t=24579
H, L = 0x26, 0xFF
read opcode 0xE at 0xF at t=24580
thread 'main' panicked at 'unsupported opcode: E', src/main.rs:108:21
I finally pulled the instruction pointer i
out from a local variable to the struct, and replaced most uses with a .pop_instruction()
method which increments it and returns the u8
at the address it pointed to. The next opcode, 0x0E
, was a simple 8-bit load:
// 8-bit loads
// LD nn,n
// Put value nn into n.
0x06 => {
// LD B, n
let n = self.pop_instruction();
self.set_b(n);
}
0x0E => {
// LD C, n
let n = self.pop_instruction();
self.set_c(n);
}
0x16 => {
// LD D, n
let n = self.pop_instruction();
self.set_d(n);
}
0x1E => {
// LD E, n
let n = self.pop_instruction();
self.set_e(n);
}
At the end of the day, I have:
; $0007 is 0x32 at t=24573
memory[HL] = A; HL -= 1
memory[0x8001] = 0x00
video_ram[0x01] = 0x00
; $0008 is 0xCB at t=24574
; opcode_2 = 0x7C
BIT 7, H ; Z = false
; $000A is 0x20 at t=24575
JR NZ, n=-5 ; Z = false
; $0007 is 0x32 at t=24576
memory[HL] = A; HL -= 1
memory[0x8000] = 0x00
video_ram[0x00] = 0x00
; $0008 is 0xCB at t=24577
; opcode_2 = 0x7C
BIT 7, H ; Z = true
; $000A is 0x20 at t=24578
JR NZ, n=-5 ; Z = true
; $000C is 0x21 at t=24579
H, L = 0x26, 0xFF
; $000F is 0x0E at t=24580
LD C, n = 0x11
; $0011 is 0x3E at t=24581
thread 'main' panicked at 'unsupported opcode: 3E', src/main.rs:173:21