gb4e
gb4e is a GameBoy emulator I started working on at the end of 2020. I have made two previous attempts at creating a GameBoy emulator which both failed.
This time, I took a different approach and built the emulator to be much more debuggable and testable right off the bat. In my previous attempts I would try to implement every single opcode and then just run the emulator and hope for the best. This approach always resulted in a screen full of garbage and no way to know where things were going wrong. This time, I built a debugger before I started implementing any opcodes, and added each opcode one by one while constantly running them through the debugger to ensure they worked as expected.
I also wrote the opcodes in a way where they do not immediately mutate the state of the emulated machine, but instead return command objects which are later applied to the machine state. This has made it easier to test each opcode in isolation.
An opcode implementation might look like this:
template <RegisterName DST, RegisterName SRC>
InstructionResult Ld(GbCpuState const * state, MemoryState const * memory)
{
Register constexpr dstReg(DST);
Register constexpr srcReg(SRC);
static_assert(dstReg.Is8Bit() && srcReg.Is8Bit(), "LD r8, r8: Both registers must be 8-bit");
u8 dstPrevValue = state->Get8BitRegisterValue(dstReg);
u8 srcValue = state->Get8BitRegisterValue(srcReg);
return InstructionResult(RegisterWrite(dstReg, dstPrevValue, srcValue), 1, 1);
}
And a test for it like this:
TEST Instr_Ld_B_A()
{
using namespace gb4e;
auto applier = Ld<RegisterName::B, RegisterName::A>;
GbCpuState state;
MemoryStateFake memory;
auto regA = GetRegister(RegisterName::A);
auto regB = GetRegister(RegisterName::B);
state.Set8BitRegisterValue(regA, 123);
state.Set8BitRegisterValue(regB, 100);
auto result = applier(&state, &memory);
ASSERT(!result.GetFlagSet().has_value());
ASSERT(result.GetMemoryWrites().empty());
ASSERT_EQ(1, result.GetRegisterWrites().size());
auto regWrite = result.GetRegisterWrites()[0];
ASSERT_EQ(RegisterName::B, regWrite.GetRegister().GetRegisterName());
ASSERT_EQ(100, regWrite.GetBytePreviousValue());
ASSERT_EQ(123, regWrite.GetByteValue());
ASSERT_EQ(1, result.GetConsumedBytes());
ASSERT_EQ(1, result.GetConsumedCycles());
PASS();
}
It took only a few days to get through the DMG Bootrom which felt like a huge achievement. I have really enjoyed this part of the process, where I step through each instruction in the debugger, implementing each new instruction I encounter one at a time.
After getting through the bootrom I started working towards getting Tetris working which is still work in progress.
The toughest part so far has been the graphics and audio. Audio programming in particular is something I have a really tough time wrapping my head around. Fortunately I found the beautiful MiniGBS project from which I adapted most of the audio code and which has seemed to work out well.