04 · Road to 60
The road to sixty
F-Zero X runs its logic at 60 Hz and the port never lowered that. Every frame the CPU walks the game's display list, translates it, and feeds the PICA. The road to 60 was the road from a 35–58 ms frame build to under 16.7 ms, measured first in the emulator and then, from 08-21, on the console. This page is that road, lever by lever.
Where the frame went: the CPU-bound verdict
The measurement pass of 08-20 settled the strategy. GPU drawing time was flat at 0.4 ms per frame and the loop never once waited on the PICA; CPU build time was 26 to 58 ms. Fill rate, model decimation and texture compression were struck from the plan as irrelevant to frame time (the whole race used 8 KB of texture bytes and 957 vertices per frame). The bottleneck was the software display-list interpreter and the bridge that feeds it. Every lever from then on attacked CPU time, and every profile was reported as a set of named buckets:
| Bucket | What it is |
|---|---|
br | the bridge pre-pass: converting the game's raw display list into the runtime's command form, resolving addresses |
dsp | interpreter dispatch: walking the converted list and executing every command |
vtx | vertex transform: projecting N64 vertices to clip space on the CPU |
tri | per-triangle work: tile extents, shader lookup, clip parameters, packed VBO emission |
imp | texture import: looking up or uploading textures for a draw |
drw | issuing the draw to citro3d, including the stereo target switch |
The emulator era: 08-14 → 08-21
Before the console arrived every number was an Azahar proxy, known to be pessimistic on menus and unrepresentative in absolute terms. The shape of the work still held.
| Date | Lever | Measured (emulator) |
|---|---|---|
| 08-14 | First race | 12–20 fps race, 15–20 fps menus, debug logging on |
| 08-20 | Frame pacer double-throttle fix, HLE audio producer to core 2, malloc histogram | measurement pass: CPU 26–58 ms vs GPU 0.4 ms |
| 08-21 | Loadblock discovery. Per-opcode profiler named G_LOADBLOCK at 22.34 ms per frame: each 4 KB load did 512 shared_ptr copies (a thousand atomic RMWs), a 4 KB memcpy and a live getenv | F3 22.34 → 2.21 ms; dispatch 27.6 → 8.9 ms; menu wall 35–38 → 16.7–19.9 ms. User: "blazing fast" |
| 08-21 | Flat dispatch table replacing libultraship's layered handler tables | neutral on time, enabled per-opcode profiling |
| 08-21 night | S7 interpreter memo (raw dispatch, geo-diag gate, tri-state memo), async frame-mirror copy, vblank-stall skip, upload thrash 155 → 0.5 per frame, resolutions 93 → 0 | menu 15 → 19.9 fps; race build 26–58 → 24–46 ms; title wall 34 → 21–27 ms |
| 08-21 | Texrect state fast path (batch same-state runs) | E4 texrect bucket 6–9.7 ms attacked |
Hardware, before the campaign: 08-21 → 08-28
| Date | Build | Hardware result (user + log) |
|---|---|---|
| 08-21 | First .cia and .3dsx, rspolish | 35–55 fps; median 48–50, p95 58, max 59 over 8 races; crowd dips 25–35. "wow it runs amazing" |
| 08-27 | Fleet drop: traffic grind (bridge region cache, binary-search asset lookup, pipesync no-op), sky wedge, CCMUX 11, stereo armed, shadow | "performance is much better! 3D works great, doesnt seem to affect performance much at all"; one hard crash (filelog OOM, fixed same day) |
| 08-28 | Touch menu, full-bleed display, rival detail option, triloop packed VBO, leak fix (upstream LUS bug) | median 51, p95 60, max 60; MINIMAL rival detail = +8–9 fps in crowds, floor ~40 (was 25–35) |
| 08-28 | GPU vertex transform moonshot | imperceptible on hardware; venue floor loses texture mapping (24-bit uniform precision). Shelved. |
The LOCKED-60 campaign: 09-01 → 09-03
Five hardware rounds in three days, each a staged branch with a test plan, each measured from the console's own once-per-second fps beats in the log. Stereo was accidentally off in rounds 1 and 2 and on from round 3, which makes the later gains larger than they look.
| Round | What it carried | Conditions | Beats | Median | p10 | At cap |
|---|---|---|---|---|---|---|
| Round 1 | Mainline before the campaign | stereo off · trace off | 754 beats · 63 min | 51.3 | 39.2 | 9% |
| Round 2 | Bridge memos (brfast) | stereo off · trace off | 78 beats · 6.5 min | 57.2 | 46.6 | 29% |
| Round 3 | HUD atlas · triangle memo · TMEM · audio | stereo on · trace off | 237 beats · 20 min | 56.0 | 48.0 | 32% |
| Round 4 | Render thread on core 2 (pipe) | stereo on · trace off | 58 beats · 5 min | 57.6 | 44.6 | 14% |
| Round 5 | Ahead mode · bridge on main · auto rival detail | stereo on · trace off · full GP | 229 beats · 19 min | 59.6 | 51.4 | 53% |
Where the milliseconds came from
Hardware profiles with the trace on (which itself inflates the numbers by a few milliseconds) on the same course, crowd windows (30 machines in view) and steady windows. Buckets in milliseconds per frame.
| Profile | Window | br | dsp | vtx | tri | imp | drw | Total |
|---|---|---|---|---|---|---|---|---|
| Campaign levers off control, same course | crowd | 2.96 | 8.26 | 1.12 | 5.58 | 0.78 | 1.48 | 20.2 |
| Campaign levers off control, same course | steady | 2.2 | 7.63 | 0.66 | 3.98 | 0.76 | 1.18 | 16.4 |
| Campaign levers on atlas · tri memo · TMEM · brfast | crowd | 3.07 | 7.84 | 1.18 | 4.64 | 0.78 | 1.24 | 18.8 |
| Campaign levers on atlas · tri memo · TMEM · brfast | steady | 2.17 | 7.35 | 0.65 | 3.22 | 0.72 | 0.79 | 14.9 |
| Render thread (pipe) core 2 renders, main waits | crowd | 3.12 | 4.89 | 1.01 | 4.21 | 0.68 | 1.16 | 15.1 |
| Render thread (pipe) core 2 renders, main waits | steady | 2.72 | 3.87 | 0.7 | 3.12 | 0.66 | 0.95 | 12.0 |
Every lever, and its verdict
| Lever | Idea | Measured | Verdict |
|---|---|---|---|
| Loadblock span store | One record per 4 KB load instead of 512 shared_ptr copies; same-content skip; getenv latch | F3 22.3 → 2.2 ms, dispatch 27.6 → 8.9 ms (emu) | mainline |
| Flat dispatch + [prof]/[profop] | Plain table dispatch; per-bucket and per-opcode timers | neutral; enabled everything after | mainline |
| S7 interpreter memos | Raw-pointer dispatch, gated geo diagnostics, tri-state memo | build 34 → 23.5 ms (emu); one 2× regression bisected out (dlcache) | mainline |
| Async frame mirror, vblank-stall skip | Menus stop waiting on a copy and on a late vblank | menu 15 → 19.9 fps; wVbl 10–16 ms → 0 (emu) | mainline |
| Texrect fast path | Batch same-state rectangle runs | E4 bucket attacked; superseded by the HUD atlas | mainline |
| Traffic grind | Bridge region cache (no svcQueryMemory per probe), binary-search asset lookup, pipesync no-op | br 19.7 → 10.4 ms, wall 55.7 → 46.7 ms (emu) | mainline |
| Prim/env value-change flush | Correctness first: flush on colour change | fixes machine tint; small cost | mainline |
| Rival detail (NATIVE/REDUCED/MINIMAL) | Bias the game's own LOD tiers for non-player machines beyond the 5 nearest | +8–9 fps in crowds on hardware | mainline |
| Triloop packed VBO | Single-write PICA-layout emission from the interpreter | kills the double vertex copy | mainline |
| Leak fix (uninitialised ColorCombinerKey::shader_id) | Upstream LUS bug: combiner map missed per draw | heap flat; hidden per-draw flush tax gone | mainline |
| Double-height stereo target | One 400×480 target, per-eye viewport, fewer PICA flushes | user observed 2D ≈ 3D on hardware; citro3d source says flush cost is GPU-side and the GPU idles | shelved |
| GPU vertex transform (moonshot) | Unlit vertex transform on the PICA vertex shader | works (220 k draws, zero fallback, −62% vtx emu); imperceptible on HW; 24-bit uniform precision breaks ±32000-unit floors | shelved |
| Bridge output cache | Cache converted display lists across frames, epoch-validated | census: 87% of walked commands are host-built per frame; ceiling ~1.5 ms | NO-GO |
| brfast (bridge memos) | Per-list facts memo, address-resolve memo, placeholder/raw-copy/range-class memos | br 11.5 → 4.9 ms (emu); HW br 6.43 → 3.73 in crowds; median 56.0 → 57.2, p10 42.0 → 47.2 | mainline |
| HUD texrect atlas (trectbatch) | Pack HUD rectangles into atlas pages; batches stay open across views | crowd draws 136 → 26 (E4 −2.6 ms emu) | mainline |
| trifast | Fast packed triangle loop; found the S7 memo had never actually hit (flag bug) | tri −15..−19% (emu); HW tri 5.58 → 4.64 ms crowd | mainline |
| tmemfast | TMEM load bookkeeping | F3 −22..−35% (emu) | mainline |
| audioprime / audioprime2 | Boot audio underrun: title font sample load blocked on a 10.7 MB inflate | store audio_table uncompressed in the o2r: preload 4.3 → 1.97 s | mainline |
| dspfast (batch-break fold) | Fold prim/env changes to avoid batch splits | census: texture switches split anyway; cannot pay | default off |
| Machine texture atlas (atlas3d) | Atlas machine-part textures to cut switches | only ~21 imports per frame split a batch; 0.1–0.5 ms best case; two thirds of machine textures wrap or mirror | default off |
| Render thread, core 2 (mode 1, pipe) | Interpreter and draws on core 2, forked at osSpTaskStartGo | HW: stable, HOME clean; median 49 → 56, p10 42 → 48 (trace on); not yet overlapping (waitMain 10–15 ms) | mainline |
| Render thread mode 2 (ahead) | DP-done acknowledged when the game parks; 2-deep backpressure; true one-frame pipeline | HW round 5: median 59.6, p10 51.4, 53% at cap | mainline |
| Bridge on main (bridgemain) | Bridge pre-pass runs on core 0 while core 2 renders | render-thread br 4 → 0 ms; brMain 3.6 ms; balanced cores | mainline |
| Auto rival detail (dynlod) | Raise the LOD tier when render > 15 ms, lower when < 12 ms; user setting is the floor | shipped in round 5; thresholds untuned on HW | mainline |
| Render-thread-owned texture cache | Fixes the round-5 data abort: clears become requests drained on the render thread | receipt texcacheMainMut=0; awaiting HW | mainline |
What is left
Round 5 is the last measured state: 53% of beats at the cap and a median a hair under 60. The floor is still the crowd start. The remaining known levers, in the order the research ranked them: double command buffers on the render thread (CPU build and GPU are still serialised on core 2), a texture import-lookup memo (imp 0.7 ms, 83 imports per frame), tuning the auto-LOD thresholds against hardware, and, if ever wanted, GPU vertex transform phase 2 on lit machine geometry with a large-coordinate CPU fallback.