devlog.

Project · Test cartridge

GBSelfTest

One file. Boot it on any emulator or on a real console and it runs 102 checks unattended, then reports the verdict twice — as text on the screen, and as the same text out of the link port so a machine can read it with nobody watching. No host, no reference images, no setup. And not one expected value in it was measured from an emulator.

102
Checks, eleven areas
100/102
SameBoy, DMG
68/102
Peanut-GB
0
Reference images needed

Measured 24 Aug 2026. The spread is the property this project exists to have — a suite everybody passes measures nothing.

SameBoy · the reference

A Game Boy screen listing eleven test areas, each marked OK, with a total line reading 100 of 102 and the word Passed.
Every area green, 100/102 with two skipped. This is the whole user interface.

Peanut-GB · built for speed

A Game Boy screen listing the same eleven areas with several marked FAIL, a total of 68 of 102, and a list of failing check codes.
Per-area failures, 68/102, then the failing codes and where to read about each.

What it does

The rule

No expected value from an emulator

Every expectation is one of two things.

  • Computed a second, independent way on the machine itself. The adder is checked against a counter built from INC HL, which sets no flags and goes through a different unit. The logic operations are checked against De Morgan's law. The rotates are checked by the fact that eight of them are the identity. DAA is checked against decimal arithmetic done a digit at a time
  • A documented constant that is a fact about the hardware — a register's read-back mask, an instruction's published cycle count, the 456 cycles in a scanline
  • A behaviour that can be checked neither way is listed as untested, with the reason, in the coverage statement the cartridge prints at the end of every run

The report

A code, a measurement, somewhere to read

  • The screen is 20 columns by 18 rows, so it carries the area verdicts and a list of codes. The link port carries the whole story
  • Each failure names what it saw, what it wanted, a hypothesis about the missing behaviour in hardware terms, and a URL
  • Codes are never reused and never renumbered, so one printed by a two-year-old build still means what it meant
  • A host that only wants the verdict can stop at Passed or Failed — Blargg's convention, so an existing runner needs no changes

What it can see

Rendering, without a framebuffer

  • A cartridge cannot see the screen. But mode 3 lasts exactly as long as the fetcher takes, and the fetcher takes longer when there is more to draw
  • So the cartridge lays out a scene whose cost is documented and measures how long mode 3 actually took — a real test of rendering with no reference image in it
  • A coincidence interrupt is armed on one scanline; the handler runs a variable number of NOPs before reading STAT once, and a binary search finds the machine cycle the mode changes on. The delay from interrupt to first NOP is unknown and does not matter, because every result is the difference between two edges
  • In practice this is the most discriminating thing here: it separates a renderer that draws a whole line at once from one that models the fetcher

What nobody else checks

Catching a shortcut, not just a mistake

An emulator that advances its peripherals once per instruction still runs every clock at the right rate. The frame is still 70,224 cycles, the timer still ticks correctly, the interrupt still arrives exactly once. Nothing measured over a run of instructions can see it. Two checks look at phase inside an instruction instead:

  • GB-CYC-07 reads one register twice from the same starting phase, once with a three-cycle instruction and once with a four-cycle one. Over four one-cycle delays the longer must overtake the shorter exactly once. A batched machine sees both reads at the instruction's start, and it never overtakes
  • GB-CYC-08 lets the timer overflow inside a sled of NOPs and has the handler read the return address, which names the instruction the interrupt landed on. Entering one cycle later must move that landing back by one instruction, eight times over. On a batched machine the landing stops moving

The scoreboard

Not a league table. Every implementation here is doing what it set out to do — TerminalGB's two rows are one emulator with two renderers behind it, and the eight-row gap between them is the fetcher model alone. Peanut-GB is built to run a Game Boy on a microcontroller. These rows are the range the checks can tell apart.

ImplementationResult
SameBoy, DMG100 / 102, 2 skipped
SameBoy, MGB (Pocket)100 / 102, 2 skipped
SameBoy, CGB-E97 / 102, 5 skipped
SameBoy, AGB98 / 102, 4 skipped
TerminalGB, per-dot renderer94 / 102, 2 skipped
TerminalGB, whole-scanline renderer86 / 102, 3 skipped
Peanut-GB68 / 102, 3 skipped

A skip is not a pass and not a failure. Two of the 102 report rather than judge — they print the number they saw and leave the verdict to a reader who has hardware, because no published reference settles the right answer. The rest are rules that do not apply to the console in the slot. Each prints its own reason, so a skipped row is never silent.

What a check has to be

Limits worth knowing

Writing


Where these numbers come from

The scoreboard was measured on 24 Aug 2026 by the project's own tools/scoreboard.sh, which runs every row and names the exact revision of each emulator: SameBoy 213a12c, TerminalGB 2467c65, Peanut-GB 8e65698. Nothing is vendored — each runner fetches, pins and builds its own emulator. Both screenshots come straight out of the emulators' own framebuffers and CI fails if they have drifted from what a run produces.

The cartridge contains no third-party source, no boot-ROM extract and no game data: the font is drawn for it, the register definitions are written out from the public-domain Pan Docs, and the whole thing is MIT so anybody can take it, ship it and change it.

← All projects