Done is where the evidence runs out

· FourShades


FourShades is finished. It plays Game Boy games, with sound, and you can play it in your browser without installing anything.

cpu instructions  499 / 500
test roms         155 / 165

Twelve of the thirteen test groups are complete. The screen tests are the thirteenth, at 20 of 30.

I could keep going. I’m not going to, and the reason is the most useful thing I learned building this: ten of those failures cannot be fixed without evidence that does not exist in the project. Not “would be hard”. Cannot. Three of them are provably impossible against the references I have, and I can show you why.

The one that contradicts five tests I pass

daid/ppu_scanline_bgp wants three CPU cycles somewhere in the chain that runs when a scanline begins: LY becomes 0 → the LYC comparison raises an interrupt → the CPU leaves halt → the interrupt dispatches. Its photograph, taken from real hardware, disagrees with my emulator by a uniform twelve dots across the whole line.

Every single link in that chain is already pinned by a different hardware-verified test that currently passes — stat_lyc_onoff, halt_ime1_timing, di_timing, intr_timing, and two more. There is nowhere to put three cycles that isn’t already measured by something else.

So the honest position is not “this is hard”. It is: one photograph disagrees with five other photographs, and I don’t know which is wrong. Making it pass would mean breaking five tests to fix one, and the whole value of a scoreboard is that you can’t do that.

The two that contradict each other

Better still: two Mealybug tests disagree with each other about screen column 2.

m3_lcdc_bg_map_change needs the tile-index read to land outside an eight-dot window. m3_lcdc_tile_sel_change needs both bitplane reads inside it. Within a single fetch the reads happen in order — tile index, then low byte, then high byte — the undisturbed tile-index read is already inside that window, and an object can only ever delay the fetcher, never advance it. So moving the tile-index read later pushes the bitplanes later still, out of the window the other test demands.

That was checked exhaustively rather than argued: every trigger dot from 89 to 139, every subset of the object penalty’s dots. Bands 0 through 15 all have solutions. Bands 16 and 17 have none.

Two photographs of the same hardware, and no emulator can satisfy both.

What the tests turned out to be

The best hours went into discovering that several of these ROMs are not what they look like.

m3_bgp_change_sprites looks like a sprite test. It is a dot ruler: one object per eight-line band at OAM X = 1 through 18, with the background palette rewritten at four known dots. Read the seams instead of the verdict and it tells you, per band, exactly what an object costs. It said the hardware charges the full documented penalty to the pixels — and gives the fetcher three dots back. One constant, two jobs, and it had been in the wrong one.

ashiepaws/strikethrough looks like a rendering test. It is an OAM-DMA-versus-object-scan race. Finding that out took writing a disassembler: the ROM parks forty objects on one scanline, then starts a DMA that overwrites them while the PPU is scanning them. Real hardware draws exactly one of the forty. I draw ten, because I evaluate all forty in a single instant where the hardware reads one every two dots.

And the last thing I fixed, which I like most: the eight pixels a Game Boy throws away at the start of every scanline are real pixels. They’re at screen x = −8 to −1, they’re the line’s first eight ticks of the pixel clock, and pixel 0 is the ninth. That means an object at OAM X = 1 is fetched seven dots earlier than one at X = 8 — even though the documentation charges them an identical penalty and both interrupt the same pixel. Nobody’s docs say that. Three separate test ROMs insisted on it.

The sound bug I could not see

The emulator passed all twelve sound tests before it made a single audible noise, because none of those tests needs a speaker. When I finally wired it to one, I wrote a test that drives a real machine, sets a channel to a known frequency, and counts zero crossings in a second of samples. 440.000 Hz measured against 439.839 expected, first run.

Then someone listened to it and said the sound lagged by about half a second.

Every instrument said the queue was healthy. They were all measuring the steady state. The bug was in the transient: the emulator’s frame loop runs until the PPU completes a frame, and the PPU completes no frames while the screen is off — which is what happens at every screen transition in every game. One pass would emulate half a second of machine time and dump twenty thousand samples at once. My drift correction adjusted by one sample per frame, about sixty a second, because it was designed for two crystals drifting apart. Draining that backlog would have taken six minutes.

The fix took four lines: above a few frames, stop pretending a backlog is drift and throw it away. One 33-millisecond dropout per screen transition instead of a lag that never goes away.

I mention it because it is the exact failure this whole project is about. A measurement that is real, correct, and pointed at the wrong thing is more dangerous than no measurement — and the only reason it got caught is that someone listened.

In the browser

The emulator core has no SDL in it, no file access, and no operating system calls. That was a rule from the first week, and it cost something to hold. It paid for itself in an afternoon: the browser build needed zero changes to the core. 62 KB of WebAssembly, 96 KB for the whole thing.

I checked it the way everything else here gets checked — not by looking at it. The browser’s framebuffer for dmg-acid2 hashes to exactly the same 23,040 bytes as the photograph taken off real hardware.

What “done” means

Ten tests fail. Every one of them is written up in known-divergences.md with the evidence and what would overturn it. Several are failing deliberately, because the alternative was fitting a constant to a single reference or breaking a test that real hardware verified.

A 165 out of 165 built that way would be worth less than this 155. The number isn’t the point; the number being true is the point. CI re-runs both suites on every commit and fails the build if the published scoreboard disagrees with a fresh run, so it can’t quietly drift away from reality.

If you have a third reference for any of those ten — or a real DMG and a logic analyser — I would genuinely like to hear from you. That is the only thing that moves them now.

← All posts