The console

Both chips on one clock, and the standard test ROMs run with a real CPU attached

The glue came first: the handful of parts on the NES-001 mainboard besides the two chips (the address decoder, the PPU’s address latch, the two RAMs, the controller port buffers with the controller behind them, the reset chain). Each is a few lines of code checked against its datasheet by its own test, and each is labelled as written by hand, since none of them comes from a netlist. We got two of them wrong the first time, and the tests said so. Then the console: the 2A03’s fast core and the fast PPU on one master half-step counter, the CPU stepping every twelve and the PPU every eight. They start at the alignment we measured off the two switch-level chips’ own clock dividers (cpu_phase 4, ppu_phase 3, one of the 24 ways the dividers can power up, and the one every run records). The plumbing check runs a test cartridge for 6 frames and confirms eight counts per dot, one dot short on odd frames, the same picture as the standalone PPU, and one NMI a frame counted in RAM. It runs at 125 to 140 frames a second on one core, 2.1 to 2.3 times real time. The alignment check then tests the place the whole project is about, where the two chips meet, in two ways. First, the PPU’s real NMI is made to land around a BRK at 8 offsets a cycle apart, and the console’s CPU is compared half-cycle for half-cycle with the switch-level 6502 driven by the same edge: 960 of them agree, the vector taken, the pushes, the timing. Second, we measured the $2002 read race on the switch-level PPU at every half-step, reading the way the console reads, and checked the console’s reads against that table under all 24 alignments: 145 reads around the flag’s set and 185 around its clear, every half-step of both windows reached, every outcome the chip’s. Then the check the whole project was built toward, against something real: blargg’s test ROMs run through the entire console, the first real programs these fast chips had run for millions of cycles. The CPU timing test passes; 16 of 16 instruction tests pass, every official and unofficial opcode; 11 of 11 sprite-hit tests pass, the double-height one since the fast PPU's tall-sprite rule was measured on the switch-level chip and checked dot for dot; 5 of 10 vblank and NMI timing tests pass. The rest are one or two dots off the documented console, and they all come down to one question: its NMI reaches the CPU about two dots later than our two chips allow, each checked against its own measured timing, and only a scope on a real board can settle it. 8 of 8 sound tests pass, six of them only after we measured each miss on the switch-level 2A03 and wrote the fix from that: the $4017 write’s parity jitter and its immediate clock, a status latched a half-step after the bus asks, an IRQ flag that stays set for three cycles, and a DMC byte counted where its read lands. What the ROMs found is the point of running them. The fast CPU had played back every recorded trace exactly and still had misses no trace had covered (a carry that rides an undriven bus line into the next instruction, a shift’s carry read from the wrong capture, three opcodes whose result is a bus fight the switch model settles its own way, the half-cycle at which an interrupt input is sampled, which the alignment check caught, and a byte latched later than the bus asks for it), and the fast PPU had four more. We found each one by running the switch-level chip and its fast version side by side on the ROM until they disagreed, measured the right behaviour on the switch-level chip, fixed the fast one, and added a test that goes red without the fix. The account is the N5 report; the play test is waiting on a cartridge.

The console's frames through the television model, and a capture of them scored

The picture runs through ntsc-crt’s chain (at its tag v0.2.18), with two things the console adds: the order of the frames, and the colour subcarrier’s phase carried from one frame to the next. The odd frame’s short line moves that phase, so the console hands over whether each frame is odd or even, not just its dots. Each frame is encoded the way an NES encodes it, decoded by the three-line comb filter, and run through the CRT stages at the settings we chose. Two checks cover it. A console frame through that chain comes out the same as the standalone fast PPU’s frame of the same test scene through the same chain, on every decoded sample (1474560 components equal, a OddShort frame, 768 by 720 on the screen). And the phase after 12 console frames, 5 of them short, is what the timing grid’s arithmetic gives for that sequence (phase 8; forcing every frame even gives a different value and turns the check red). The colour-bars cartridge the real comparison needs paints with the PPU’s rendering switched off, which nobody had asked the fast PPU about. Measured on the switch-level chip, the picture with rendering off is the palette entry the address register points at, and its timing against a mid-line write now has its own test there. Then the capture path, the machine half of the comparison the real console will join. A bars cartridge runs through the console; its frames go through ntsc-crt’s model of a capture card and are recovered exactly as a real recording would be; and the synthesis goes through the model’s own front end, so both sides share the same band limit. Every flat region is then scored against that synthesis through the identical decoder, 5 dots in from its edges, a distance worked out from the decoder’s colour filter rather than picked. We wrote the tolerances down before the run: luma within 0.01, hue within 1 degree, saturation within 5 percent. The cartridge is our own, because blargg’s bars are sixteen dots wide and the decoder needs five to settle: thirty-two-dot cells of the twelve hues at one luma row plus the backdrop, the row stepping every two seconds, one run scored per row. luma row 1: 13 of 13 regions hold all three (worst luma 0.0007, hue 0.4 degrees, saturation 0.0011, rate found to +0.0 ppm); luma row 2: 13 of 13 regions hold all three (worst luma 0.0007, hue 0.4 degrees, saturation 0.0012, rate found to +0.0 ppm); luma row 3: 13 of 13 regions hold all three (worst luma 0.0005, hue 0.4 degrees, saturation 0.0005, rate found to +0.0 ppm); luma row 0: 13 of 13 regions hold all three (worst luma 0.0006, hue 0.4 degrees, saturation 0.0008, rate found to +0.0 ppm). The roundtrip closes. The first runs found three faults in our measuring tools before they found anything about the picture: a level reference one histogram bin too coarse, a dark picture mistaken for blanking, and the darkest colours’ chroma dips mistaken for sync edges. Each was fixed in ntsc-crt and given a new tag. The real recording of the same cartridge on the real console is on the bench list, and the cartridge exists for it now; the account is the N6 report.

The console's sound through the board's audio stage, checked with blargg's mixer tests and set beside his recordings

The 2A03’s five output codes leave the chip after every CPU half-cycle and go through the two DACs, using the nesdev table we have carried since first sound, which is borrowed rather than measured and labelled that way. What happens next is on the NES-001 schematic, read directly: each audio pin pulled down by 100 ohms (the table’s own “plus 100”), the two pins summed through 20K and 12K (the ratio the table’s two constants already carry), and a coupling capacitor into a 74HC04 inverter kept linear by a 47K feedback resistor with 220 pF across it. So, on the way to the jack: a high-pass with a time constant of 7.50 ms, a gain of -2.350 with the inverter’s sign, a low-pass at 10.34 microseconds, then a windowed-sinc resampler to 48 kHz at the exact rational times. The stage is checked against that arithmetic (a step decays by 0.3679 per time constant, and a 10 kHz tone against a 200 Hz one comes through at 0.8401 where the values give 0.8433). What we do not model, and say so: the inverter’s finite open-loop gain and its rails, and the table’s absolute volts, which is one scale factor a scope recording will supply. The check against real hardware is blargg’s: four mixer ROMs, each playing a channel while the DMC plays its inverse, so a correct mixer cancels to near silence between two reference beeps. Each ran through the whole console, and the worst 100 ms window of each test, as a share of the beep, must stay under 5 percent (the DMC’s step alone is about two). The noise ROM is judged on its tone, since it fades the noise out on purpose. The console first, then blargg’s recording of the same ROM on real hardware, measured by the same code: square 2.4 percent against 6.1; triangle 2.7 percent against 3.0; noise 21.1 percent against 19.0; dmc 3.1 percent against 5.6. Triangle and noise agree with the real console to a fraction of a percent. On real hardware, square and dmc leave twice the console’s residual, and that residual is a tone: the real pulse and DMC DACs depart from the table’s curves by more than the table departs from blargg’s inverse. That is a question for the scope, so we record it rather than test against it. Mixing through the wiki’s linear approximation instead turns the check red, at a third of the beep. The account is the N7 report; recording the real console’s audio output is on the bench list.

The console in a window, its picture on the GPU matching the CPU version, and a second home in the browser

We measured a frame’s time on one core before writing anything, and it said where the work had to go: the console and the encoder fit on a core, and the comb decode with the five CRT stages did not fit anywhere on the CPU. Those two are now eight compute passes on the GPU, with every constant uploaded from the decoder and the CRT settings rather than typed in. They are checked against the signal path’s own CPU version on every component of every pixel of three consecutive frames, the last one black so that the screen’s afterglow shows. With our chosen settings the worst component differs by 4.8e-7 and the mean by 1.2e-8; with the mask and the geometry switched on, 2.8e-5 and 1.9e-7. The tolerance, stated first, was 1e-3 and 1e-5. A frame takes 0.87 ms on the NVIDIA GeForce RTX 3070, upload included. Skipping the afterglow turns the check red. The window is a Linux program. The console and its sound run on their own thread, moving forward by whole frames as the signal path’s drift policy decides from the wall clock, counting repeated and dropped frames and never stretching time. The display encodes each new frame and runs the GPU picture; sound goes to the audio device; the keyboard, or a gamepad through gilrs, is controller 1. We test the loop on a fake clock: at exactly the frame period, 60 ticks run 59 new frames with 1 repeated and 0 dropped; at half the period 31 of 60 ticks show the previous frame again; at twice the period it drops 19 in 20. It ran under a virtual display on this box; a real screen, a speaker and a pair of hands are still to try at the desk. The second home is the browser: the console and its sound compiled to WebAssembly, measured under node on full_palette.nes: 300 frames in 4.19 s, 71.7 frames a second, 1.19 times real time, 798.7 sound samples a frame. It has a page now: the console runs here, on a cartridge from your own disk, through the same signal path the signal bench runs, with the drift counters shown raw. The account is the N8 report.

Every number here comes from re-running the tests

The figures below come from running the console repository’s own suite again on 2026-09-29, at the recorded commit; this page reads only what that run wrote.

nes suite: 87 tests passinstruction tests: 16 of 16 passthe console: 2.1 to 2.3x real timenes commit: ffa239e

The console

Both chips on one board: the glue, the machine running test ROMs, then its picture, its sound and the window it plays in.

  • The mainboard's glue N4 report: The handful of parts on the NES-001 board between the chips, each checked against its datasheet and labelled as written by us.
  • Both chips on one clock N5 report: The console running: the two fast chips on one clock, the timing between them checked against the transistor-level chips, and blargg's test ROMs run end to end.
  • Planning the console's picture N6 plan: The picture through the signal path, and how a captured frame will be compared, with the tolerances written down first.
  • The console's picture N6 report: The picture through the signal path, the screen with rendering off measured on the chip, and a captured frame scored, figures recorded and not fitted.
  • Planning the console's sound N7 plan: The sound through the board's audio stage, read off the schematic.
  • The console's sound N7 report: The sound through the board's audio stage, blargg's mixer test ROMs cancelling, and his recordings of real hardware beside ours.
  • Planning the console's window N8 plan: The window the console plays in, the picture on the GPU, the pacing, and a second target in the browser.
  • The console in a window N8 report: The window built and checked without a screen, the GPU picture matching the CPU's, and the browser build measured.