Five hardware rounds of a 30-machine Grand Prix, measured in 5-second frame-rate beats on a real New 3DS. What each change did, in the order it shipped.
Each row is one hardware session under play conditions (no profiler tracing). The watchdog thread counts frames every 5 seconds, so every bar is a share of those beats. The orange bar is the 60 fps cap. Markers show the median and the 10th percentile, the floor a player feels in crowds.
Three traced sessions, same course, profiler on (which adds ~2 ms per frame, so compare bars to each other, not to 16.7). Crowd frames are windows with 800–1400 transformed vertices; steady race frames 400–800. The render thread moved this whole bar off the game's core; the ahead mode then let the game run a frame in front of it.
Every lever shipped with a kill switch and a receipt line in the log, was A/B'd in the emulator on frame-aligned windows, then verified on hardware before merging. Emulator deltas are per crowd frame; hardware deltas are what the card measured.
| Lever | What it changes | Emulator | Hardware | Status |
|---|---|---|---|---|
| Bridge memos brfast | Each sub-display-list was classified and scanned 3–4× per frame; per-list facts and address resolution are now memoized. | br 11.5 → 4.9 ms | br 6.4 → 3.7 ms median 46 → 49 | mainline |
| HUD texture atlas trectbatch | HUD sprites (digits, bars, portraits) packed into shared pages with exact clamp gutters; runs of texrects become one draw. LRU page eviction. | draws 136 → 26 texrect −2.6 ms | draws/frame 67 → 55 | mainline |
| Triangle memo fix trifast | The per-triangle draw-state memo had never hit once (a texture-unit flag was never cleared). Fixed, plus an exact packed vertex loop. | tri −17 % | tri 5.6 → 4.6 ms | mainline |
| TMEM span fast path tmemfast | LOADBLOCK bookkeeping walked every word of the replaced span; now a live-span list. Also fixed an 8-byte alignment that tripled mirror copies. | LOADBLOCK −22 % | within dispatch bucket | mainline |
| Boot audio audioprime · o2r | Ring primed before playback; the 10.7 MB audio table stored uncompressed so boot preload is a read, not an inflate. | underruns 3/3 → 0/3 | preload 4.3 → 2.0 s | mainline |
| Render thread renderthread=1 | Display-list interpretation and citro3d draws run on core 2 at a priority below audio; game logic stays on core 0. First N64 port to do this. | n/a (emulator time-slices core 2) | median 49 → 56 p10 42 → 48 | mainline |
| Ahead mode renderthread=2 | "DP done" is acknowledged when the game parks on it, so logic runs a frame ahead; fences on every RDRAM writer in between. | waitMain ≈ render − logic | see Round 5 | round 5 |
| Bridge on main bridgemain | The command pre-pass runs on the submitting core while core 2 draws the previous frame. | render-side br 4.0 → 0 | see Round 5 | round 5 |
| Auto rival detail rival_detail_auto | Render time over 15 ms raises the distant-rival LOD tier; under 12 ms lowers it. The manual setting is the floor. | tier moves both ways | see Round 5 | round 5 |
| Prim/env colour folding dspfast | Colour changes become batch segments instead of flushes. | ≈ 0 (texture switch splits anyway) | — | off by default |
| Machine texture atlas atlas3d | Same page machinery for machine parts. Census: only 8 clamp-safe switches per frame; two thirds of machine textures wrap or mirror. | draws −7, ≈ 0 ms | — | off by default |
| Bridge output cache | Cache translated lists across frames. | ceiling 1.5 ms | — | not built: 87 % of commands are rebuilt every frame |
| GPU vertex transform | N64 T&L in the PICA200 vertex shader. | vtx −62 % (unlit) | imperceptible | shelved: vtx is ~1 ms on hardware |
| Symptom on hardware | Cause | Fix |
|---|---|---|
| HOME button crashed the game | Audio threads kept driving the DSP while the OS finalized it. | Audio threads park on suspend and resume cleanly; clock and syscore share re-applied on restore. |
| Close from the HOME menu hung | Close arrives as a cancel with no restore; the drain thread hot-spun on a shared sticky event and starved the producer on its core. | One park event per thread; flushed exit receipts per teardown step. |
| HUD showed one portrait everywhere | The triangle lever's fast vertex loop lacked the atlas page offset. Each lever had only been tested alone. | Offset in both loops; combined race screenshots are now part of staging. |
| Touch-menu tab bar scrolled away | Stray console writes reaching the bottom row. | A filter device drops all stdout/stderr while the menu owns the screen. |
| Boot audio hiccup | Title-font sample load blocked 2.1 s behind a CPU-bound inflate of the audio table. | Archive stores it uncompressed. |
| Crash after a full GP in the menus (Round 5) | Render thread's texture-cache LRU spliced while the main core mutated the cache during podium/venue loads. | in progress cache becomes render-thread-owned; main only enqueues. |
Frame rate comes from the watchdog thread's beat line: frames rendered in each 5-second window, divided by five. That smooths single-frame hitches but is honest about sustained dips, which is what the 10th percentile captures. Sessions differ in length and content (Round 1 is an hour of mixed play; Round 5 is one full Grand Prix), so the histograms are normalized to shares.
The CPU buckets come from the port's sectioned profiler on hardware. Tracing costs about 2 ms per frame, so those three sessions are compared with each other only. Stereo 3D was off in the first two rounds and on afterwards, which doubles draw calls; the like-for-like pairs in the ledger were always measured with the same 3D setting.