F-Zero X · New Nintendo 3DS · performance ledger

Locked Sixty

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.

Frame-rate distribution, round by round

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.

Where a crowd frame's CPU goes

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.

The change ledger

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.

LeverWhat it changesEmulatorHardwareStatus
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 msbr 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 → 55mainline
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 msmainline
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 bucketmainline
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/3preload 4.3 → 2.0 smainline
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 − logicsee Round 5round 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 → 0see Round 5round 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 wayssee Round 5round 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 cacheCache translated lists across frames.ceiling 1.5 ms—not built: 87 % of commands are rebuilt every frame
GPU vertex transformN64 T&L in the PICA200 vertex shader.vtx −62 % (unlit)imperceptibleshelved: vtx is ~1 ms on hardware

Fixed along the way

Symptom on hardwareCauseFix
HOME button crashed the gameAudio 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 hungClose 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 everywhereThe 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 awayStray console writes reaching the bottom row.A filter device drops all stdout/stderr while the menu owns the screen.
Boot audio hiccupTitle-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.

How to read the numbers

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.

Emulator numbers never rank levers. Azahar time-slices the 3DS cores on one host thread and its per-instruction costs differ; it was used for correctness, A/B deltas on frame-aligned windows, and screenshot parity. Every lever's magnitude claim above comes from the card.
Build lineage: mainline feat/3ds-hwaudio; Round 5 is feat/3ds-hwtest4 @ 978f31d. Fan-made, educational, no game assets distributed; not affiliated with Nintendo.