gameboy-sharp

A Game Boy (DMG) emulator in C#. A frontend-free core, so the whole machine runs and is verified headless against standard test ROMs.

gameboy-sharp
TL;DR

A Game Boy (DMG model) emulator written in C#. The core is separated from the frontend and checked headless against standard test ROMs, so other people's games run bit-for-bit.

Overview

gameboy-sharp is an emulator of the original Game Boy, the DMG model, written in C#. The task sounds simple: make games released thirty years ago run without changing a single byte. The difficulty hides in the word "faithfully" - the emulator has to imitate the hardware precisely enough that the original code notices nothing.

We built the core so it knows nothing about a window or sound. The whole machine is a pure library that returns a frame buffer and audio samples, and only the frontend turns them into what you see and hear. This is what lets the whole console run without a screen and compare its result against what should come out.

Bugs in an emulator almost never crash loudly. The game boots, the game runs - only the sound is a touch too high, or the character moves a pixel too far. There is no exception, no crash, just subtly wrong behaviour that you cannot tell apart from correct by eye.

That changes the whole way of working. You cannot trust the impression that it "looks fine", because that impression is exactly what lies most often. The only thing you can trust is comparing the machine's state against a value you know in advance - and the whole project is built around that.

The heart of the machine: the CPU

At the heart of the console is the Game Boy's CPU, a Z80 variant. Every instruction is a read of an opcode byte and doing exactly what the original would, including the cycle count that instruction costs. Timing is not a detail - it is what decides whether sound and picture are synchronised the way they are on real hardware.

Cpu.cs · csharp
byte opcode = ReadByte(PC++);

switch (opcode) {
    case 0x00: cycles += 4; break;
    case 0x3E: A = ReadByte(PC++); cycles += 8; break;
    case 0xC3: PC = ReadWord(); cycles += 16; break;
    default: cycles += Execute(opcode); break;
}

An instruction and its cost in cycles

OpcodeInstructionCycles
0x00NOP4
0x3ELD A, d88
0xC3JP a1616
i
Note

The core knows nothing about a window or sound - it is a pure machine. The frontend only gets a frame buffer and audio samples. That is what lets the whole console run without a screen and compare the result against what is expected, cycle by cycle.

Tests down to the cycle

The emulation community maintains sets of test ROMs that check individual registers, flags and timing to the cycle. We run them headless and compare the machine's state against what should come out. That is the only honest way to say an emulator is correct, not just "works on my machine".

That is exactly why the core was cut away from the frontend. If the machine needed a window, every test would have to be clicked through by hand. Since it is a pure library, the test ROMs run automatically and the verdict is binary: it matches or it does not.

The result: other people's games bit-for-bit

What comes out is an emulator that runs other people's games and passes standard test ROMs, not just one that shows the Nintendo logo and stops. The sound is where it should be, the character moves the number of pixels it should, and correctness is proven by tests, not by impression. It is a project built out of love for old hardware and taken to a state where you can see it.

More projects

More work from the same category - see how we tackle similar challenges.

Have a similar project?

Get in touch - a quote is free and comes back within an hour.