Post-mortem · New 3DS 2026-08-11 → 09-03 · Native fork 2026-09-07 → 10-19 (v1.0.0) · After v1.0.0 10-20 → 10-26 (v1.1.0)

G-Diffuser on 3DS

F-Zero X, ported from the libultraship PC runtime to the New Nintendo 3DS, reaching 59–60 fps with stereoscopic 3D, by one person directing a fleet of Claude agents over 24 days — then forked to a native renderer that also reached 59–60 fps on an Old 3DS development build, brought the 64DD Expansion Kit to the New 3DS, and added a bottom-screen dashboard, shipping as v1.0.0 (stereoscopic 3D ships as not yet a locked 60 fps on either console — see RELEASE-NOTES.md), then, a week later, v1.1.0: multiplayer, one-submission stereo and Old 3DS memory fixes.

  1. Overview — What was built, in one screen
  2. Author's notes — The one page written by a human
  3. Every step — The chronology, New 3DS to v1.1.0
  4. Road to 60 — The New 3DS performance campaign, round by round
  5. Road to 60, again — The Old 3DS needed a new renderer: how it works, and what shipped
  6. Pushing the boundaries — How it compares to the field
  7. Difficulties — The fights worth retelling
  8. Frontier — What this port does that others in its position haven't
  9. Agentic workflow — How the fleet was run, and how the ship was steered
  10. Cost — Tokens, dollars, subagents

Sources: git history, docs/research, hardware logs, local Claude Code transcripts. Dollar figures use published API list prices. Generated from docs/postmortem/.

01 · Overview

F-Zero X on 3DS — Old and New, native, with the 64DD Expansion Kit

How G-Diffuser, the PC port of the F-Zero X decompilation, got onto the Nintendo 3DS: one person directing, Claude agents writing all of the code, 11 weeks from first research to multiplayer.

  • v0.9.0 2026-09-04 New 3DS and New 2DS XL only: a 59.6 fps hardware median and stereoscopic 3D, after a 24-day sprint.
  • v1.0.0 2026-10-19 Old 3DS and 2DS support, a native renderer as the default on both consoles, the 64DD Expansion Kit on New 3DS, and the DASH bottom screen.
  • v1.1.0 2026-10-26 Multiplayer for 2–4 players over local wireless, Wi-Fi or the internet; native menus; both 3D eyes in one GPU submission. Release notes in docs/RELEASE-NOTES.md.

The numbers

60fps
New 3DS, 3D race: all 120 one-second windows at 59–61
60fps
Old 3DS, 3D race: median window; 85% at 60, tenth percentile 56
22.59 B
tokens across all sessions and agents, 08-11 to 10-26
$19,559
at API list prices ($15,567 billing cache writes as logged)
777
human prompts; 588 subagents and 1,288 workflow agents did the work
181k
lines of port code under port/3ds, tests included

Hardware: card-6 test logs, build ba0692e, 10-26 (1P vs CPU field, 3D on); how the numbers got there is on Road to 60. Tokens and cost: session transcripts to 10-26, method on Cost.

On hardware

v1.0 on a New 3DS: Select Mode with the Expansion Kit entries, the G-DIFFUSER idle view on the bottom screen
v1.0, New 3DS: the Expansion Kit's Select Mode (COURSE EDIT, CREATE MACHINE); the bottom screen is DASH's idle view.
v1.0 on a New 3DS: a race on the top screen, the DASH race dashboard on the bottom
v1.0, New 3DS, mid-race: DASH on the bottom screen with energy, position, lap times and the course map.
v1.1.0 work, emulator: a four-player Mute City race at the countdown, with the DASH showing standings and the NET indicator on the bottom screen
v1.1.0 work, multiplayer, emulator capture (not a photo of hardware): a four-player race on Mute City at the countdown. Each console shows its own racer; the DASH shows the standings and the NET indicator.
v1.1.0 work, emulator: Select Mode with VS Battle chosen and the bottom-screen MULTIPLAYER menu offering Play Nearby and Play Online
v1.1.0 work, emulator capture: picking VS opens the MULTIPLAYER menu on the bottom screen, nearby (local wireless or same Wi-Fi) or online with a room code.

The clips and photos below are from v0.9 (2026-09), before DASH replaced the debug console on the bottom screen.

open clip
v0.9: Grand Prix on a New 3DS. The bottom screen is the port's debug menu: 59.8 fps, heap, build id.
open clip
v0.9: handheld, mid-race. Native 60 Hz pacing; no interpolation.
F-Zero X title screen on the New 3DS
Title screen, launched from the Homebrew Launcher (.3dsx).
Select Mode screen
Select Mode.
Select Machine screen
Select Machine.
Blue Falcon stats
Blue Falcon.

What was built

Three console builds from one source tree:

  • Old 3DS / 2DSF-Zero X
  • New 3DS / New 2DS XLF-Zero X
  • New 3DS + Expansion KitF-Zero X with the 64DD add-on

Each installs from the HOME menu (.cia) or the Homebrew Launcher (.3dsx). No ROM or disk image ships: players extract their own.

The whole game

  • Grand Prix, Time Attack, Practice, Death Race, VS and the machine screens.
  • With the Expansion Kit: Course Edit and Create Machine.

Speed

  • New 3DS: 60 fps races in 2D and 3D.
  • Old 3DS: 2D races at or near 60; 3D races mostly hold 60.
  • The heaviest Old 3DS courses can still drop to 30 in 3D.

The console, used properly

  • Stereoscopic 3D on the slider.
  • Music and effects on the DSP.
  • A bottom-screen dashboard (DASH).
  • Clean HOME, sleep and power-off.

Multiplayer (v1.1.0 work)

  • 2–4 players, each on their own console.
  • Nearby or online.

Both consoles draw through the same native renderer by default. The New 3DS's first-phase render-thread path is still built in as an option; Frontier and Road to 60 cover it.

The stack

  • decomp: the F-Zero X decompilation, unmodified upstream, with a small set of patches on top.
  • libultraship: the N64 runtime that PC ports of decompiled games are built on. The port carries a trimmed copy with its 3DS changes folded in. It loads the game's assets; its original renderer remains as a fallback.
  • port/3ds: the port itself, 181k lines: rendering, stereo, audio, input, menus, lifecycle, multiplayer and packaging.
  • the native renderer: N64 PC ports normally translate the game's drawing commands into GPU calls one at a time, every frame. This port instead writes the 3DS GPU's own commands directly, from data prepared ahead of time, for races, menus and most transitions: nearly everything on screen. Only a few rare screens, such as the course editor and the ending, still take the old route. No other port in this family does this, and it is what lets the Old 3DS keep up. See The road to 60, all over again.

How it went

The first phase (08-11 → 09-03) ported the game to the New 3DS. The second (09-07 → 10-19, not originally planned) brought it to the Old 3DS through a native renderer and shipped v1.0.0. Then a week of v1.1.0 work.

v0.9 = New 3DS only (first phase)v1.0 = Old 3DS (second phase, the native fork)v1.1 = polish and online multiplayer
DatesStageWhat happened
08-11 → 08-14v0.9From research to a raceFeasibility dossier, a plan for a team of agents, five parallel foundation streams, six boot bugs; first race at 12–20 fps in the emulator.
08-20 → 08-22v0.9Playable, then on a consoleFog, HUD, livery, shadow and sky fixed; menus from 27 to 60 fps; first hardware run 08-21 ("wow it runs amazing"), audio the same evening.
08-27 → 08-28v0.9Product gradeStereo 3D armed, touch menu, rival detail; a GPU vertex-transform moonshot built and shelved; README and release package.
09-01 → 09-02v0.9Lifecycle and the last big leverHOME crash and close-from-HOME hang fixed; per-frame display-list re-scanning replaced by memoized results; the LOCKED-60 campaign launched.
09-02 → 09-03v0.9Multi-coreRender thread on core 2, one frame ahead; median 59.6 fps with 53% of frames at the cap. v0.9.0.
09-07 → 09-29v1.0Make it fitOld 3DS restarted; build squeezed into its 64 MB app region. First boot on a real Old 3DS: about 10 fps.
09-29 → 09-30v1.0Find the real costsNarrower audio lock (−20 ms a frame); frame skipping ends the slow motion; a retained renderer wins in the emulator only.
09-30 → 10-01v1.0The probeHardware counters: the limit is instruction-cache misses, not instruction count. A working pose-replay fallback is shelved: "we should go for a real 60!"
10-01 → 10-02v1.0Native rendererSmall race loops sized to the 16 KB instruction cache. 2D Grand Prix at a 16.5 ms median frame.
10-03 → 10-08v1.03D at 60Per-eye resubmit: 59–60 fps in 3D on most courses; the heaviest can fall to 30.
10-08 → 10-19v1.0ReleaseExpansion Kit verified tick-for-tick, released for New 3DS only (too slow on Old); DASH ships; adversarial review; v1.0.0.
10-20 → 10-26v1.1After v1.0.02–4 player multiplayer; one GPU submission for both eyes (Old 3DS 3D menu 40.2 → 44.6 fps); HOME-close lock fixed.

See Agentic workflow for how the second phase was run and The road to 60, all over again for the Old 3DS campaign, card by card.

Read on

Every number on these pages has a source: git history, the research documents under docs/research/, the hardware logs, or the session transcripts the Cost page counts. Where a value is an estimate (the dollar figures, which apply published API list prices to exact token counts), the assumption is stated next to it.

02 · Author's notes

A note from the human

Everything else on this site was written by the agent that did the work. This page was not.

Hello reader. This is the only section of this project written by a real human. Isn't that kind of neat? Well, I guess it depends on how you feel about all of these changes in our lives. In this section, I'll quickly detail my thoughts on this 100% AI generated port, its reflection on the direction of software development, and my own experience in using Claude Code to create it.


Awhile back I saw Chris Lewis make a post about using Claude to assist in the Snowboard Kids 2 decompilation. It got me thinking: what a perfect use-case for a coding agent. It's a long running, tedious, endeavor of classification and definition.

I realized that one day I'd like to port F-Zero X to 3DS using a coding agent when it was decompiled/ported. So when G-Diffuser port was released based on the Inspectredc Decompilation, I gave it a shot. I pulled a bunch of adjacent repositories & documentation together, fed it to Claude using deep research, read the master plan and gave it the OK to spin up a bunch of subagents. For 3 weeks, I babysat and assumed a role somewhere between a Product Owner and a QA tester.

  1. Claude suggests an implementation strategy to either fix a bug or improve performance.
  2. I approve the suggestion, push back, or do research/implore research if we seem to hit a dead end.
  3. Claude works and validates in Azahar emulator.
  4. Claude provides a build and I test on 3DS.
  5. I report back and provide debug logs. Loop continues.

This continued until it was 'done'. It still totally blows my mind that after 24 days I can play F-Zero X in 3D at a near solid 60fps.


Since my first experience using ChatGPT in 2022, I was filled with total existential dread. I saw my livelihood threatened, as this skill I had dedicated my life to became immediately devalued. Since then, I've used LLM chatbots & coding tools extensively and I've oscillated between feelings of fascination, fear, humility, intrigue, reward, and emptiness.

This project revealed to me how much things have changed. The complexity of this project cannot be overstated: this is a marvel. This is nothing short of a miracle. And it was done by a machine, with no human intervention on the code. I never read a line. I never made a true technical decision.

I learned how to program 15 years ago with the dream of contributing projects like this, and since then I've come to a stark realization: while I COULD plausibly port F-Zero X to the 3DS by hand, the time in my life IS finite, and when weighed against other life pursuits such as making an original game, it would not serve me well. I'm sure the journey would make me a better programmer. I'm sure I would learn so much about graphics rendering, address-space heuristics, the 3DS architecture, edge device profiling, etc. And I wholeheartedly thank & admire everyone whose work this is built off of, many of you are nameless but overwhelmingly appreciated.

The (terrifying) fact is: this would've taken me a few thousand hours of my life. For professionals, this might've cost nearly 80x to make (actually, even more, because Claude is currently subsidized).

So to any naysayers, feel free to not play. I understand. The ethics of LLMs are muddy and their incoming impact on society is understated. This project, in my opinion, serves as proof of the latter. However, to anyone who has ever dreamed of F-Zero X 3DS, enjoy.


Update, 2026-10-27:

Hello again, reader. Since I published this about 2 months ago, it has taken on a bit of a life of its own and been a catalyst for discussion around Agentic development. There's been plenty of drive-by dismissals, some appreciation, some curiosity, and, best of all, some folks who walked away a bit more enlightened by the nuance.

This project has continued to be fun, and I'm still shocked how the boundaries can be pushed. I now consider this project 'feature complete' aside from some bug fixes.

All of this is still keeping me up at night. This project was a great learning experience for me, and has furthered my ability to use Claude far better than I could before. My high level understanding is more concrete now, but when I'm pressed on edge cases in problems, I'm clueless, because I'm not engaging enough with implementation details. I have severe fatigue from the massive amounts of code churned out and my ability to comprehend it all just by reading carefully. It's a practice that is tiring but it's my new job, I suppose.

To anyone who has enjoyed the project, thank you. The support kept me going. I think it came along really well and I hope to race some of you online.

Thanks especially to my girlfriend, Stace, who had to hear the Mute City theme way too many times and sat beside me while I toiled away, surrounded on the bed by many gadgets.

And thanks to TuxSH, a veteran in the 3DS scene, and jonhohle, a contributing member to the decomp scene. They challenged the integrity of the project on Hacker News. I understand a project stands on their shoulders, and so I respect their advice and principles greatly, even if we disagree on this project. I am humbled that they even took a look, and I mean no disrespect.

03 · Every step

Step by step: twenty-four days, then forty-three more, then one week

Five chapters, one per tab: the New 3DS port (five bursts between three credit pauses), the native fork, the Old 3DS, shipping v1.0.0, and the week after it. Dates are local, from git and the session transcripts; quotes are the user's own words at the time.

Every entry was re-checked against what came after it. Where later evidence revised a number, a root cause or a decision, the entry carries a numbered Later note; Held marks a claim that was checked and stands.

1 · New 3DS · 08-11 → 09-03
commitsuser messages
030609012008-11 · commits to the 3DS port: 208-11 · user messages: 608-1108-12 · commits to the 3DS port: 1808-12 · user messages: 008-1208-13 · commits to the 3DS port: 2508-13 · user messages: 1508-1308-14 · commits to the 3DS port: 1908-14 · user messages: 3608-1408-15 · commits to the 3DS port: 208-15 · user messages: 308-1508-16 · commits to the 3DS port: 008-16 · user messages: 008-1608-17 · commits to the 3DS port: 008-17 · user messages: 008-1708-18 · commits to the 3DS port: 008-18 · user messages: 008-1808-19 · commits to the 3DS port: 008-19 · user messages: 008-1908-20 · commits to the 3DS port: 5008-20 · user messages: 1008-2008-21 · commits to the 3DS port: 5308-21 · user messages: 4608-2108-22 · commits to the 3DS port: 008-22 · user messages: 408-2208-23 · commits to the 3DS port: 008-23 · user messages: 208-2308-24 · commits to the 3DS port: 008-24 · user messages: 008-2408-25 · commits to the 3DS port: 008-25 · user messages: 008-2508-26 · commits to the 3DS port: 008-26 · user messages: 008-2608-27 · commits to the 3DS port: 3808-27 · user messages: 2008-2708-28 · commits to the 3DS port: 908-28 · user messages: 1108-2808-29 · commits to the 3DS port: 008-29 · user messages: 008-2908-30 · commits to the 3DS port: 008-30 · user messages: 008-3008-31 · commits to the 3DS port: 008-31 · user messages: 308-3109-01 · commits to the 3DS port: 1409-01 · user messages: 709-0109-02 · commits to the 3DS port: 10709-02 · user messages: 2409-0209-03 · commits to the 3DS port: 1409-03 · user messages: 2409-03per day
Chapter 1, per day. Commits touching the port and messages typed by the user. The gaps are three pauses when the weekly allowance for the regular model (the "Fable" tier) ran out.

Burst 1 · Research to first race (08-11 → 08-14)

  1. 08-11 03:16
    Deep research: is a 3DS port feasible?24 Verdict: New 3DS only; the Old 3DS 64 MB region cannot hold it.Later (09-25 → 10-19): it fit under simulation on 09-25, booted on an Old 3DS on 09-29, and shipped for the Old 3DS in v1.0.0. — this page, chapter 3; RELEASE-NOTES.md v1.0.0
  2. 08-11 03:44
    Plan for a team of agents "can we segment plan so we can have multiple work trees and multiple contributing agents ?" A memory budget, a five-stream plan and a reviewer agent.
  3. 08-11 → 08-12
    Phase 0 and the carve spike libultraship compiles on devkitARM with a 13-line newlib patch; Phase 1 streams merged 08-12 21:45.
  4. 08-13
    "start the crew", then black screens Full game linked 01:33; then bzero, 64DD, path mangling, double swap, the SETTIMG guard, a DMA misroute. 13:17: "i saw some random shapes briefly".
  5. 08-14 01:01
    First race "omg it works ! i can race ! it looks messed up and runs at 12 to 20fps but it works!" (after a race-time bad_alloc fix).
  6. 08-14 04:26
    Hardware prep, CI, docs CIA packaging, a hardware runbook, local CI; research on 60 fps and on stereo 3D.
  7. 08-14 14:06
    Pause 1 "we ran out of fable credits until next week". Fog (pink road) still open.

Burst 2 · Playable, then hardware (08-20 → 08-22)

  1. 08-20 02:55
    Resume; the road appears Fog solved (per-vertex blend, not the depth LUT); fog, GPU profiling, stereo, perf and culling merged.
  2. 08-20 05:38
    "run as many subagents on plausible tasks"23 A fleet on HUD, shadow, sky, tunnel, km/h, pop-in. The measurement pass: CPU-bound, not GPU-bound.Held (09-01 → 10-24): an emulator measurement at the time (GPU 0.4 ms vs CPU 26–58 ms); hardware agreed (GPU 5.6 vs build 16.3 ms), and the Old 3DS probe agreed on 10-24. — locked60-campaign.md; NATIVE-RENDERER.md ceiling_probe
  3. 08-20 22:45
    The HUD bug After eight wrong fixes: the bridge's ILP32 high-bits backstop dropped 13 SETTIMG a frame. "omg finally!!"
  4. 08-21 13:03
    "menus … still crazy slow at around 10fps" A per-opcode profiler named G_LOADBLOCK at 22.34 ms a frame; fixed within the hour. "blazing fast on the UI".
  5. 08-21 15:54
    First hardware run "wow it runs amazing ! … sound doesn't work btw". Audio fixed by 21:22 after eleven SD trips (the ROM-side driver needed host porting).
  6. 08-21 22:49
    "framerate at a crispy 60. then... maybe 3D?" Race-start hitch and decal fixes. Hardware median 48–50 fps, p95 58.
  7. 08-22 01:31
    Pause 2 "ran out of tokens again". Checkpoint 854b9d6.

Burst 3 · Product grade (08-27 → 08-28)

  1. 08-27 06:21
    Fleet drop Traffic grind (bridge 19.7 → 10.4 ms), sky wedge, CCMUX 11, off-axis stereo, shadow audit.
  2. 08-27 16:00
    Hardware with stereo22 "3D works great, doesnt seem to affect performance much"; first hard crash: the filelog grew without bound, fixed with a retained ring.Later (09-02 → 10-02): the 256 KB cap truncated a session (raised to 1 MB, 09-02); the ring then caused 26–270 ms and 1.1–2.8 s SD-write hitches (09-30), fixed by an async, dropping writer. — 2344e88; difficulties.html
  3. 08-27 16:53
    Touch menu and full bleed "the bottom screen should have a menu for these features"; full-bleed made default. 19:30: "i did the first GP!"
  4. 08-28 00:18
    Rival detail, packed VBO, HOME exit21 "minimal appears to be a 8 to 9 fps gain !"Later (10-19): the New 3DS preset runs full rival detail; automatic scaling is used on the Old 3DS. — gdx3ds_preset_n60.c; RELEASE-NOTES.md v1.0.0
  5. 08-28 04:27
    "moonshot! lets go for it"20 GPU vertex transform in one session: works, imperceptible on hardware, breaks the venue floor. Shelved.Later (09-29): the broken floor was traced to float24 cancellation on world-space vertices (up to ±32,000); a machines-only transform was rebuilt for the Old 3DS and ships in both presets. — o3dsgpuxf-progress.md; gdx3ds_preset_n60.c
  6. 08-28 13:17
    README and pause 3 README-3DS and the release package. Then the third pause.

Burst 4 · The last big lever (09-01 → 09-02)

  1. 09-01 21:27
    "the goal to 60fps"19 Bridge output cache NO-GO (87% of commands are host-built each frame); brfast memos instead, bridge 11.5 → 4.9 ms. Research: nobody has shipped beyond the per-frame walk.Later (10-02): the native race renderer (10-01/02) skips building the game's display-list sections on rendered ticks and draws from GPU-resident meshes. — native.html
  2. 09-01 23:53
    "the home button crashes the game!" Suspend-path audit and audio park gate; the first combined A/B test card.
  3. 09-02 14:02
    Hardware round 2 HOME works; close-from-HOME hang fixed. brfast: median 57.2 vs 56.0, p10 47.2 vs 42.0.
  4. 09-02 14:41
    LOCKED-60 campaign HUD atlas, TMEM bookkeeping, per-triangle cost, boot audio: four agents, landed by 13:40.
  5. 09-02 18:35
    "VERY slow. massive step back" A combined-lever HUD break; fixed, and "screenshot the combined levers before staging" became a rule.
  6. 09-02 21:56
    Rounds 3 and 4 Median 56, p10 48, 32% at the cap with stereo on; trace-on A/B steady 16.4 → 14.9 ms.
  7. 09-02 21:59
    Research 218 Core 2 usable, core 3 not, no precedent for cross-core render pipelining. Agents on a dispatch census, boot audio, a render thread.Later (10-19): the render thread shipped in v0.9.0 and was left out of the v1.0.0 presets (above). — gdx3ds_preset_n60.c

Burst 5 · Multi-core (09-02 night → 09-03)

  1. 09-02 → 09-03
    Render thread, sync → pipe → ahead Forked at osSpTaskStartGo; ahead mode acks DP-done when the game parks. Pipe mode on hardware: median 49 → 56.
  2. 09-03 13:20
    Round 5: median 59.6, p10 51.4, 53% at the cap17 Ahead mode, bridge on main, auto rival detail. Then a hard crash (texture-cache LRU on core 2), fixed by render-thread ownership.Later (10-19 → 10-26): v1.0.0's tuned presets set renderthread = 0: the native race path fits 16.7 ms on one core. Card 6: New 3DS 3D races at 60 in every window without it. — gdx3ds_preset_n60.c; o3ds-card6-analysis.md §4
  3. 09-03 13:46 → 16:32
    Menu scale and stereo markers Machine-select 1.6× too large (viewport fold); 1P/2P markers anchored at the ship's depth.
  4. 09-03 18:08
    This post-mortem "lets write up a whole post mortem on this project."
  5. 09-04
    State of play (v0.9.0)16 Ahead mode, bridge-on-main, auto rival detail and the crash fixes all merged. The GPU vertex transform stayed shelved; the tunnel-roof bug (#27) was still open at v1.0.0.Later (09-29 → 10-19): a machines-only gpuxf = 1 (the floor stays on the CPU) is on in both consoles' auto presets in v1.0.0 and v1.1.0. — gdx3ds_preset_n60.c; o3dsgpuxf-progress.md
2 · Native fork · 09-07 → 09-23
08917826635509-07 · commits: 009-0709-08 · commits: 009-0809-09 · commits: 009-0909-10 · commits: 009-1009-11 · commits: 009-1109-12 · commits: 4509-1209-13 · commits: 3409-1309-14 · commits: 009-1409-15 · commits: 009-1509-16 · commits: 009-1609-17 · commits: 4209-1709-18 · commits: 009-1809-19 · commits: 009-1909-20 · commits: 1809-2009-21 · commits: 009-2109-22 · commits: 009-2209-23 · commits: 31709-23commits per day
Chapter 2, per day. Commits on the native-fork branch. No session transcripts were archived for this period, so commits are the only daily signal.

A feasibility pass on 09-07 found the fix should start with the development loop, not the renderer: no network path to the console, no frame oracle, no desktop reference build. Over two weeks that loop was rebuilt (a network control and log link, input-recorder determinism, a Linux reference render container), and the first renderer tier landed: an owned F3DEX2 interpreter replacing libultraship's, a native race path (track, machines, sky, HUD, effects), and the first 64DD Expansion Kit pass. In full on The road to 60, all over again.

3 · Old 3DS · 09-24 → 10-08
0469113718309-24 · commits: 12209-2409-25 · commits: 809-2509-26 · commits: 8409-2609-27 · commits: 3109-2709-28 · commits: 009-2809-29 · commits: 12709-2909-30 · commits: 12009-3010-01 · commits: 16310-0110-02 · commits: 9510-0210-03 · commits: 2010-0310-04 · commits: 010-0410-05 · commits: 010-0510-06 · commits: 010-0610-07 · commits: 7810-0710-08 · commits: 13910-08commits per day
Chapter 3, per day. Commits on the native-fork branch; per-card numbers are on The road to 60, all over again.
  1. 09-24
    The milestone reopens A stale note said the user owned no Old 3DS; they did. Under a simulated 64 MB grant: RDRAM arena halved to 8 MiB, audio_table streamed.
  2. 09-25
    It fits, in simulation15 Replay 49/49 under memtype=0 with 2.65 MiB to spare.Later (10-16 → 10-26): a 10-16 hardware capture peaked at heapUsed ≈ 24.2 MB; card 6 logged a race watchdog at heapFree=3264. — STATUS.md; o3ds-card6-analysis.md §5.1
  3. 09-28
    The console says memtype=0 Matches the simulation byte for byte; the shipped RDRAM-16 build dies in its boot preload.
  4. 09-29 00:10
    "it ran ! at like 9fps" Wall p50 96 ms, GPU busy 3.6 ms. "is there anything we can do to avoid the display lists"
  5. 09-29 day
    The parallel wave Audio-lock wait 25.8 → 0.2 ms, the HOME freeze (backlight session), bit-exact ARMv6 SIMD audio, the slow-motion discovery.
  6. 09-29 13:09
    "we want this running at 60fps" A design sub-orchestrator and five research and probe subagents.
  7. 09-29 15:16
    "playable but nausea inducing"14 Frame skipping: logic 57–60 ticks/s, drawn 19–28 fps. Draw cost measured at 10.9 µs per draw.Held (10-26): DrawProbe measured 10.9 µs flat over 30–300 draws (2,926 cycles). A uniform-free draw is 8.0 µs; a trimmed citro3d reached ~6.8 µs in the emulator, unmeasured on hardware; race draws now bypass it. — cpu-profile-answer.md §A.2
  8. 09-29 19:39
    First release candidate does not boot13 Cold size-optimised code crashes, stock boots; code generation reverted. The latent bug was never found.Held (10-26): still unproven; code generation stays stock and hot-first ordering was never re-tested on hardware. — rel1crash-progress.md; agent-D-cpu-cuts.md
  9. 09-30 01:10
    Second candidate boots, hitching12 "like 15fps ... a lot of awkward hitching": synchronous SD log flushes, 26–270 ms on main.Later (10-02 → 10-19): a full ring later forced 1.1–2.8 s writes; fixed by an async writer, then a log that drops lines rather than block. v1.0.0's own figure still shows the gap (57.3 vs 51.9 fps). — difficulties.html; RELEASE-NOTES.md v1.0.0
  10. 09-30 02:09
    "start as many agents as necessary" Seven agents hit the session limit together and were resumed; then the disk filled.
  11. 09-30 → 10-01
    A hardware profiler11 CPI 4.34, 43.6% fetch stalls; a probe found the cliff from CPI 1.06 at 16 KB of hot code to 4.1 at 24 KB.Held (10-07 → 10-08): native-renderer builds cut instructions ~3.6× and I-misses ~4×, but CPI rose to 5.4–6.4 with I-fetch stalls at 28–32%: less code, not faster code. — cpu-profile-answer.md §A.3
  12. 10-01 08:57
    "we should go for a real 60!" Pose replay (40 images/s, artifacts) set aside against a research verdict of "marginal"; an I-cache-first native race renderer begins.
  13. 10-01 22:41
    First native-renderer build freezes A verify mode rolled back the live GPU command queue; hardware only.
  14. 10-02 09:28
    "time trial was silk 60fps" 199,472 draws verified, 0 bad; Grand Prix 45–50 with log hitches. Saves after the first never persisted (fixed).
  15. 10-02 23:19
    "omg it ran amazing!" Card 3: race median 16.5 ms, 0 hitches. "lets focus on 3D at 60fps".
  16. 10-03
    3D, first pass10 Black sky on an interim build, then "60fps for 2d for both", 3D in the 40s and 50s. The right eye is interleaved per chunk.Later (10-26): each chunk's two eyes became one queue entry (CMDBUF_JUMP): Old 3DS race entries 9 → 7, 3D menus 40.2 → 44.6 fps; v1.1.0 default. — ba0692e; a2905ed
  17. 10-03 → 10-07
    Rate-limit pause One branch's uncommitted edits recovered on 10-07.
  18. 10-07 22:51
    3D, second pass Mute City ~60 in 3D with end-mode pacing; heavier venues 16–18 ms.
  19. 10-08 02:08
    "all 3 crashed" Bottom-screen framebuffer in VRAM, which a CIA maps read-only for the CPU; the emulator does not enforce it.
  20. 10-08 02:56
    "did a long play on main, was good!"9 VRAM change reverted: 3D Grand Prix race windows at 59–60 fps; 0 bad draws.Later (10-24 → 10-26): a development card, not the shipped build. A 16-machine race later ran 53.7 fps; one submission brought the 1P race to 59.7–59.8. — cpu-profile-answer.md §A.4; o3ds-card6-analysis.md
4 · Shipping · 10-08 → 10-19
0397811715610-08 · commits: 13910-0810-09 · commits: 010-0910-10 · commits: 010-1010-11 · commits: 010-1110-12 · commits: 010-1210-13 · commits: 010-1310-14 · commits: 010-1410-15 · commits: 7910-1510-16 · commits: 3110-1610-17 · commits: 2610-1710-18 · commits: 1110-1810-19 · commits: 910-19commits per day
Chapter 4, per day. Commits on the native-fork branch; the week-long gap is a rate-limit pause.
  1. 10-08
    Card 4 closes the 3D push8 59–60 fps in 3D Grand Prix race windows on a development card; the rest of v1.0.0 followed. Commit- and document-sourced from here.Later (10-19 → 10-26): v1.0.0 shipped 3D as “not yet a locked 60 fps”; a heavier 16-machine 1P race measured 53.7 fps (10-24); card 6's baseline ran 56.4 / 59.3 and one submission 59.7–59.8 (10-26). — RELEASE-NOTES.md; cpu-profile-answer.md §A.4
  2. 10-07 → 10-08
    Expansion Kit: own title and save The last blocker to a second, side-by-side install.
  3. 10-08 → 10-15
    Rate-limit pause The longest gap of the project.
  4. 10-15 → 10-17
    The bottom screen becomes DASH7 Lap, position, energy, splits, composited into the framebuffer at well under a millisecond of CPU time.Later (10-23): on Old 3DS hardware the DASH cost 4.2–5.6 ms a frame and was put on a 1 ms budget. — d9deb37; e99777b
  5. 10-16
    The Old 3DS memory ceiling, in simulation6 One large mid-race asset load can exhaust the budget (the generic archive loader); a hardware capture peaked near 24.2 MB. Left as a known limit.Later (10-26): v1.1.0 moved ~320 KB off the main heap and reserved the track at boot; card 6 showed the failure mode is fragmentation. Tight memory stays a known issue. — d7010a8; a2905ed; RELEASE-NOTES.md v1.1.0
  6. 10-16
    An audio-crackle hypothesis, refuted5 Vblank vs audio rate measured and ruled out; a smaller resample fix ships.Held (10-20): v1.1.0 turns the clamp and declick fixes on for the New 3DS, not yet confirmed to cure its crackle; Old 3DS residual crackle still open. — RELEASE-NOTES.md v1.1.0 Audio; 9ead784
  7. 10-18
    A New 3DS froze at the title screen The attract demo's full-field race overflowed the GPU command buffer; see The road to 60, all over again.
  8. 10-18 → 10-19
    Custom HUD mode shelved The three fixed DASH layouts ship; an idle view outside races.
  9. 10-19
    Disk streaming and Japanese inputs by default The 64DD image streams from SD on every build; Japan-region inputs accepted.
  10. 10-19
    v1.0.0 Four build targets from one tree, three downloads (Old 3DS, New 3DS, New 3DS + Expansion Kit; the Old 3DS Kit build stays from-source), replacing the New-3DS-only v0.9.0.
5 · After v1.0.0 · 10-20 → 10-26
01427415510-20 · commits: 3210-2010-21 · commits: 4910-2110-22 · commits: 3810-2210-23 · commits: 1610-2310-24 · commits: 2210-2410-25 · commits: 2410-2510-26 · commits: 1910-26commits per day
Chapter 5, per day. Commits on the release branch since the v1.0.0 tag. The Old 3DS numbers are on The road to 60, all over again.
  1. 10-20 → 10-25
    Multiplayer Local wireless, same-Wi-Fi and online rooms, lockstep races with each console showing its own racer, a relay server, version gating. Old 3DS: 2D 60 in a 16-racer online race; 3D locked to 30. See MULTIPLAYER.md.
  2. 10-24
    The Old 3DS 3D ceiling probe4 Waiting out the previous frame's GPU work first made the frame no cheaper (13.5 → 14.2 ms), so a deferred-wait redesign was dropped: 3D races here are CPU-bound.Later (10-26): card 6 found 3D menus waiting 7.66 ms a frame on the GPU queue (per-entry latency); one submission per stereo chunk took them from 40.2 to 44.6 fps. — o3ds-card6-analysis.md §3
  3. 10-26
    HOME close, fixed3 Card 5's Old 3DS hard lock was a 73 KB allocation in the 64DD save flush; the flush now allocates nothing.Later (10-26): while chasing this lock a 617 KB drop in free heap was reported as a regression. The two logs compared were a 2D multiplayer session and a 1P 3D session; the comparison was wrong, not the build. — o3ds-card6-analysis.md; 9d19a5c
  4. 10-26
    One submission for both eyes2 From a homebrew developer's public question. Card 6 A/B: Old 3DS 3D menu 40.2 → 44.6 fps, race GX entries 9 → 7, 1.5–1.8 ms of headroom; New 3DS neutral. v1.1.0 default.Held (10-26): the menu result (6 vs 9 blocks, ±0.2 fps) is solid; the race figure rests on two blocks per arm. The Old 3DS frame has 9 queue entries, not the 11 the pre-card notes assumed. — onesubmit-progress.md; o3ds-card6-analysis.md §0
  5. 10-26
    Track arena and a wider 3D sky1 The course track moves into one 777,360-byte arena reserved at boot, aimed at the fragmentation failure card 6 hit; sky, floor and clouds widened by the eye shift.Later (10-26): built for card 6's failure (a 96,768-byte allocation refused with 1.6 MB free: fragmentation, not exhaustion); tested in the emulator, not yet on hardware. — RELEASE-NOTES.md v1.1.0 known issues; o3ds-card6-analysis.md §5.1
  6. 10-26
    The bottom screen, last pass DASH layout, D-pad navigation in the multiplayer sheets, one SETTINGS button.
  7. 10-26
    v1.1.0, prepared for release This page's last entry.

04 · Road to 60

The road to sixty

F-Zero X runs its logic at 60 Hz and the port never lowered that. On 10-26 both consoles race at 60: the New 3DS with headroom to spare, the Old 3DS at the edge in 3D. Most of this page is how the New 3DS got there first, from 08-14 to 09-03, on a renderer that is no longer the default. Every number is labelled with its date and build.

Where we are now · 10-26, v1.1.0

60fps
New 3DS 1P race in 3D, every window (card 6, 10-26)
59.7fps
Old 3DS 1P race in 3D with one submission per stereo chunk (card 6, 10-26)
60 / 30
Old 3DS multiplayer: 2D at 60 (10-25), 3D locked at 30 (10-24)
59.6
New 3DS median at the end of this page's campaign (round 5, 09-03)
Console · modeNow (date, build)Then
New 3DS · 1P race, 3D60 fps in every ~1 s window; render task 2.5–3.8 ms, ~13 ms of headroom (card 6, 10-26, ba0692e; widescreen on)median 59.6, p10 51.4, 53% of beats at 60 (round 5, 09-03)
New 3DS · 1P race, 2Dno separate native-renderer figure on file (v1.0.0 notes: not yet measured); 3D, the heavier mode, holds 60median 57.2 with stereo off (round 2, 09-02)
New 3DS · menus, 3Dmedians 56–61 fps (card 6; few windows, noisy)~20 fps in the emulator (08-21)
Old 3DS · 1P race, 2D60 fps: median frame 16.5 ms, 0 hitches (card 3, 10-02); full-field GP 57.3 fps mean with diagnostic logging (card 2, 10-02)unsupported until 09-28; ~10 fps on first boot (09-29)
Old 3DS · 1P race, 3D59.7–59.8 fps with one submission per stereo chunk, baseline 56.4 / 59.3 (card 6, 10-26; two blocks per arm); a heavier 16-machine race 53.7 fps (10-24)59–60 in race windows on courses 12–16 (card 4, 10-08)
Old 3DS · menus, 3D44.6 fps, from 40.2 (card 6, 10-26)interpreter menus with frame skipping (v1.0.0)
Old 3DS · multiplayer2D: 60 fps race windows, 2 players + 14 CPU (10-25, 3c0c7b7). 3D: locked 30 by design (10-24)multiplayer is new in v1.1.0
New 3DS · multiplayerno separate figure on file—

Since v1.0.0 (10-19) both consoles run the native renderer by default ([perf] preset = auto: n60 on New 3DS, o3ds-auto on Old 3DS), with the render thread off. Sources: hwlogs/n3ds-card6, o3ds-card6, o3ds-card3, o3ds-0c2; RELEASE-NOTES.md; MULTIPLAYER.md. The renderer and the Old 3DS road are on The road to 60, all over again.

What the Old 3DS work gave back to the New 3DS

  • The default route. The native renderer, per-eye stereo replay and end-of-frame pacing are the New 3DS default since v1.0.0 (n60, render thread off). The render-thread path below is still built in behind an explicit ini.
  • Two audio levers (audio_ctx_narrow, audio_simd) are on in the New 3DS preset too, though it never lacked audio headroom.
  • Fixes for both models: a backlight/HOME freeze, a save that could be lost on power loss (an SD rename quirk), and a log that never blocks the frame.

Where we were. Everything below is the New 3DS campaign of 08-14 → 09-03 (v0.9.0), on the libultraship-derived interpreter with a render thread on core 2. Notes marked Later say what changed afterwards.

Where the frame went: the CPU-bound verdict

New 3DS · 08-20 measurement pass (emulator), confirmed on hardware 09-01

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:

BucketWhat it is
brthe bridge pre-pass: converting the game's raw display list into the runtime's command form, resolving addresses
dspinterpreter dispatch: walking the converted list and executing every command
vtxvertex transform: projecting N64 vertices to clip space on the CPU
triper-triangle work: tile extents, shader lookup, clip parameters, packed VBO emission
imptexture import: looking up or uploading textures for a draw
drwissuing 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.

DateLeverMeasured (emulator)
08-14First race12–20 fps race, 15–20 fps menus, debug logging on
08-20Frame pacer double-throttle fix, HLE audio producer to core 2, malloc histogrammeasurement pass: CPU 26–58 ms vs GPU 0.4 ms
08-21Loadblock 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 getenvF3 22.34 → 2.21 ms; dispatch 27.6 → 8.9 ms; menu wall 35–38 → 16.7–19.9 ms. User: "blazing fast"
08-21Flat dispatch table replacing libultraship's layered handler tablesneutral on time, enabled per-opcode profiling
08-21 nightS7 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 → 0menu 15 → 19.9 fps; race build 26–58 → 24–46 ms; title wall 34 → 21–27 ms
08-21Texrect state fast path (batch same-state runs)E4 texrect bucket 6–9.7 ms attacked

Hardware, before the campaign: 08-21 → 08-28

DateBuildHardware result (user + log)
08-21First .cia and .3dsx, rspolish35–55 fps; median 48–50, p95 58, max 59 over 8 races; crowd dips 25–35. "wow it runs amazing"
08-27Fleet 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-28Touch 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-28GPU vertex transform moonshotimperceptible on hardware; venue floor loses texture mapping (24-bit uniform precision). Shelved.

The LOCKED-60 campaign: 09-01 → 09-03

New 3DS hardware · v0.9.0-era libultraship interpreter build · rounds 1–5

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.

medianp10
0204060cap 60Round 1|Mainline before the campai · median fps: 51.351.3Round 1|Mainline before the campai · p10 fps: 39.239.2Round 1Mainline before the campaiRound 2|Bridge memos (brfast) · median fps: 57.257.2Round 2|Bridge memos (brfast) · p10 fps: 46.646.6Round 2Bridge memos (brfast)Round 3|HUD atlas · triangle memo · median fps: 56.056.0Round 3|HUD atlas · triangle memo · p10 fps: 48.048.0Round 3HUD atlas · triangle memo Round 4|Render thread on core 2 (p · median fps: 57.657.6Round 4|Render thread on core 2 (p · p10 fps: 44.644.6Round 4Render thread on core 2 (pRound 5|Ahead mode · bridge on mai · median fps: 59.659.6Round 5|Ahead mode · bridge on mai · p10 fps: 51.451.4Round 5Ahead mode · bridge on maifps (1-second beats)
Median and floor, round by round. Round 1 is mainline before the campaign (754 beats over 63 minutes). Round 5 is a full Grand Prix with the render thread in ahead mode, the bridge on the main core and automatic rival detail.
02550Round 1|Mainline before the campai · % of beats at 60: 99Round 1Mainline before the campaiRound 2|Bridge memos (brfast) · % of beats at 60: 2929Round 2Bridge memos (brfast)Round 3|HUD atlas · triangle memo · % of beats at 60: 3232Round 3HUD atlas · triangle memo Round 4|Render thread on core 2 (p · % of beats at 60: 1414Round 4Render thread on core 2 (pRound 5|Ahead mode · bridge on mai · % of beats at 60: 5353Round 5Ahead mode · bridge on mai% of beats ≥ 59.5 fps
Time spent at the cap. The fraction of one-second windows pinned at 60 went from one in eleven to more than half.
RoundWhat it carriedConditionsBeatsMedianp10At cap
Round 1Mainline before the campaignstereo off · trace off754 beats · 63 min51.339.29%
Round 2Bridge memos (brfast)stereo off · trace off78 beats · 6.5 min57.246.629%
Round 3HUD atlas · triangle memo · TMEM · audiostereo on · trace off237 beats · 20 min56.048.032%
Round 4Render thread on core 2 (pipe)stereo on · trace off58 beats · 5 min57.644.614%
Round 5Ahead mode · bridge on main · auto rival detailstereo on · trace off · full GP229 beats · 19 min59.651.453%
Round 1, beforeRound 5, after
0153046610–30 · Round 1: 550–30 · Round 5: 110–3030–35 · Round 1: 0030–35 · Round 5: 0030–3535–40 · Round 1: 6635–40 · Round 5: 0035–4040–45 · Round 1: 101040–45 · Round 5: 1140–4545–50 · Round 1: 191945–50 · Round 5: 3345–5050–55 · Round 1: 303050–55 · Round 5: 111150–5555–58 · Round 1: 151555–58 · Round 5: 111155–5858–59.5 · Round 1: 7758–59.5 · Round 5: 181858–59.5≥59.5 · Round 1: 99≥59.5 · Round 5: 5353≥59.5% of beats
The distribution moved, not just the median. Round 1 has a long tail below 45 fps (crowd starts, transitions). Round 5 stacks its mass in the top bin.

Where the milliseconds came from

New 3DS hardware · 09-02 → 09-03 · trace on

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.

bridgedispatchvertextriangleimportdraw
0.05.711.317.022.6Campaign levers off|crowd · bridge: 3.0Campaign levers off|crowd · dispatch: 8.3Campaign levers off|crowd · vertex: 1.1Campaign levers off|crowd · triangle: 5.6Campaign levers off|crowd · import: 0.8Campaign levers off|crowd · draw: 1.520.2Campaign levers offcrowdCampaign levers on|crowd · bridge: 3.1Campaign levers on|crowd · dispatch: 7.8Campaign levers on|crowd · vertex: 1.2Campaign levers on|crowd · triangle: 4.6Campaign levers on|crowd · import: 0.8Campaign levers on|crowd · draw: 1.218.8Campaign levers oncrowdRender thread (pipe)|crowd · bridge: 3.1Render thread (pipe)|crowd · dispatch: 4.9Render thread (pipe)|crowd · vertex: 1.0Render thread (pipe)|crowd · triangle: 4.2Render thread (pipe)|crowd · import: 0.7Render thread (pipe)|crowd · draw: 1.215.1Render thread (pipe)crowdCampaign levers off|steady · bridge: 2.2Campaign levers off|steady · dispatch: 7.6Campaign levers off|steady · vertex: 0.7Campaign levers off|steady · triangle: 4.0Campaign levers off|steady · import: 0.8Campaign levers off|steady · draw: 1.216.4Campaign levers offsteadyCampaign levers on|steady · bridge: 2.2Campaign levers on|steady · dispatch: 7.3Campaign levers on|steady · vertex: 0.7Campaign levers on|steady · triangle: 3.2Campaign levers on|steady · import: 0.7Campaign levers on|steady · draw: 0.814.9Campaign levers onsteadyRender thread (pipe)|steady · bridge: 2.7Render thread (pipe)|steady · dispatch: 3.9Render thread (pipe)|steady · vertex: 0.7Render thread (pipe)|steady · triangle: 3.1Render thread (pipe)|steady · import: 0.7Render thread (pipe)|steady · draw: 0.912.0Render thread (pipe)steadyms per frame (trace on)
Per-bucket frame time on hardware. The campaign levers shaved the triangle and dispatch buckets; the render thread took dispatch from 7.8 to 4.9 ms in crowds by overlapping it with game logic on core 2. The 16.7 ms budget line is the whole bar; anything under it is a 60 fps frame.
ProfileWindowbrdspvtxtriimpdrwTotal
Campaign levers off control, same coursecrowd2.968.261.125.580.781.4820.2
Campaign levers off control, same coursesteady2.27.630.663.980.761.1816.4
Campaign levers on atlas · tri memo · TMEM · brfastcrowd3.077.841.184.640.781.2418.8
Campaign levers on atlas · tri memo · TMEM · brfaststeady2.177.350.653.220.720.7914.9
Render thread (pipe) core 2 renders, main waitscrowd3.124.891.014.210.681.1615.1
Render thread (pipe) core 2 renders, main waitssteady2.723.870.73.120.660.9512.0

Every lever, and its verdict

Verdicts as of 09-03 (v0.9.0); later changes noted in the rows

LeverIdeaMeasuredVerdict
Loadblock span storeOne record per 4 KB load instead of 512 shared_ptr copies; same-content skip; getenv latchF3 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 timersneutral; enabled everything aftermainline
S7 interpreter memosRaw-pointer dispatch, gated geo diagnostics, tri-state memobuild 34 → 23.5 ms (emu); one 2× regression bisected out (dlcache)mainline
Async frame mirror, vblank-stall skipMenus stop waiting on a copy and on a late vblankmenu 15 → 19.9 fps; wVbl 10–16 ms → 0 (emu)mainline
Texrect fast pathBatch same-state rectangle runsE4 bucket attacked; superseded by the HUD atlasmainline
Traffic grindBridge region cache (no svcQueryMemory per probe), binary-search asset lookup, pipesync no-opbr 19.7 → 10.4 ms, wall 55.7 → 46.7 ms (emu)mainline
Prim/env value-change flushCorrectness first: flush on colour changefixes machine tint; small costmainline
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 hardwareLater (10-19): the New 3DS preset now runs full rival detail; automatic scaling is an Old 3DS lever. — RELEASE-NOTES.md v1.0.0mainline
Triloop packed VBOSingle-write PICA-layout emission from the interpreterkills the double vertex copymainline
Leak fix (uninitialised ColorCombinerKey::shader_id)Upstream LUS bug: combiner map missed per drawheap flat; hidden per-draw flush tax gonemainline
Double-height stereo targetOne 400×480 target, per-eye viewport, fewer PICA flushesuser observed 2D ≈ 3D on hardware; citro3d source says flush cost is GPU-side and the GPU idlesLater (10-26): both eyes did end up in one GPU submission, by another route (a CMDBUF_JUMP per stereo chunk): 3D menus +11% on Old 3DS, neutral on New 3DS; v1.1.0 default. — o3ds-card6-analysis.mdshelved
GPU vertex transform (moonshot)Unlit vertex transform on the PICA vertex shaderworks (220 k draws, zero fallback, −62% vtx emu); imperceptible on HW; 24-bit uniform precision breaks ±32000-unit floorsLater (09-29 → 10-19): a machines-only gpuxf = 1 ships on by default on both consoles; the floor stays on the CPU. — o3dsgpuxf-progress.md; gdx3ds_preset_n60.cshelved
Bridge output cacheCache converted display lists across frames, epoch-validatedcensus: 87% of walked commands are host-built per frame; ceiling ~1.5 msHeld (10-02): the NO-GO stands; the native fork removed the bridge from the race path instead. — native.htmlNO-GO
brfast (bridge memos)Per-list facts memo, address-resolve memo, placeholder/raw-copy/range-class memosbr 11.5 → 4.9 ms (emu); HW br 6.43 → 3.73 in crowds; median 56.0 → 57.2, p10 42.0 → 47.2mainline
HUD texrect atlas (trectbatch)Pack HUD rectangles into atlas pages; batches stay open across viewscrowd draws 136 → 26 (E4 −2.6 ms emu)mainline
trifastFast packed triangle loop; found the S7 memo had never actually hit (flag bug)tri −15..−19% (emu); HW tri 5.58 → 4.64 ms crowdmainline
tmemfastTMEM load bookkeepingF3 −22..−35% (emu)mainline
audioprime / audioprime2Boot audio underrun: title font sample load blocked on a 10.7 MB inflatestore audio_table uncompressed in the o2r: preload 4.3 → 1.97 smainline
dspfast (batch-break fold)Fold prim/env changes to avoid batch splitscensus: texture switches split anyway; cannot paydefault off
Machine texture atlas (atlas3d)Atlas machine-part textures to cut switchesonly ~21 imports per frame split a batch; 0.1–0.5 ms best case; two thirds of machine textures wrap or mirrordefault off
Render thread, core 2 (mode 1, pipe)Interpreter and draws on core 2, forked at osSpTaskStartGoHW: 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 pipelineHW round 5: median 59.6, p10 51.4, 53% at capLater (10-19 → 10-26): off by default since v1.0.0 (renderthread = 0): the native race path fits on one core, and New 3DS 3D races held 60 in every window on card 6 without it. — gdx3ds_preset_n60.c; o3ds-card6-analysis.md §4mainline
Bridge on main (bridgemain)Bridge pre-pass runs on core 0 while core 2 rendersrender-thread br 4 → 0 ms; brMain 3.6 ms; balanced coresmainline
Auto rival detail (dynlod)Raise the LOD tier when render > 15 ms, lower when < 12 ms; user setting is the floorshipped in round 5; thresholds untuned on HWLater (10-19): kept in the Old 3DS o3ds-auto preset; not used by the New 3DS preset. — RELEASE-NOTES.md v1.0.0mainline
Render-thread-owned texture cacheFixes the round-5 data abort: clears become requests drained on the render threadreceipt texcacheMainMut=0; awaiting HWLater (10-19): the render thread is off in the v1.0.0 presets, so this fix is off the default path. — gdx3ds_preset_n60.cmainline

Where this road ended

The interpreter path topped out here: a median a hair under 60 on the New 3DS, with the crowd start still the floor. For the Old 3DS it was a dead end, about 10 fps on first boot, far short of 60. That is what led to the native fork: The road to 60, all over again.

05 · Road to 60, again

The road to 60, all over again

On the Old 3DS, the original interpreter could not keep up. So from 09-07 we replaced it, piece by piece, with code that writes PICA200 commands directly. This page covers that native renderer, the Old 3DS road to 60, and what shipped with it.

~1,600
commits on the native integration branch, 09-07 → 10-19
187
feature branches, almost all merged
2
consoles from one codebase
0 / 10,466
libultraship interpreter lines that run during a race at default settings

Part 1 · The renderer

The convention

Every N64 PC port in this family renders the same way.

N64 PC PORTS · SOH, 2SHIP, STARSHIP, G-DIFFUSER PCThe gameBuilds an F3DEX2 displaylist every frame, as it didfor the N64's RSP.A CPU interpreterlibultraship's Fast3D walksevery command: matrices,transform and lighting,clipping, the combinerturned into a shader.A graphics APIOpenGL, Metal or DirectXcalls, re-emitted fromscratch each frame.A desktop GPUFast enough that nobodyminds the CPU doing theN64's geometry work.
N64 PC PORTSThe gameBuilds an F3DEX2 display list everyframe, as it did for the N64's RSP.A CPU interpreterlibultraship's Fast3D walks everycommand: matrices, transform andlighting, clipping, the combinerturned into a shader.A graphics APIOpenGL, Metal or DirectX calls,re-emitted from scratch each frame.A desktop GPUFast enough that nobody minds the CPUdoing the N64's geometry work.
The display list is rebuilt and re-walked on the CPU every frame: portable, exact, and cheap on a desktop CPU.

Where we were — v0.9.0

v0.9.0 ran that convention unchanged. On a 3DS it is CPU-bound.

V0.9.0 · 09-04The gameBuilds 32-bit F3DEX2words every frame.The bridgeWidens each commandto the interpreter's64-bit form (12,353lines).libultraship'sinterpreterEvery vertextransformed and liton the ARM11 (10,466lines).citro3d backendOne C3D_DrawArraysper batch, with stateand uniform upload.PICA200Mostly waiting forthe CPU.MEASURED COSTInstruction cache~219 KB of hot codeagainst a 16 KB L1-I:43.6% of Old 3DSrender cycles stalledon fetch.Per-draw cost10.9 µs per cleancitro3d draw on Old3DS; 72–98 µs pergame draw through theinterpreter.CPU-boundFirst Old 3DS boot: a96 ms frame, GPU busy3.6 ms of it.
V0.9.0 · 09-04The gameBuilds 32-bit F3DEX2 words everyframe.The bridgeWidens each command to theinterpreter's 64-bit form (12,353lines).libultraship's interpreterEvery vertex transformed and lit onthe ARM11 (10,466 lines).Instruction cache~219 KB of hot code against a 16 KBL1-I: 43.6% of Old 3DS render cyclesstalled on fetch.citro3d backendOne C3D_DrawArrays per batch, withstate and uniform upload.Per-draw cost10.9 µs per clean citro3d draw onOld 3DS; 72–98 µs per game drawthrough the interpreter.PICA200Mostly waiting for the CPU.CPU-boundFirst Old 3DS boot: a 96 ms frame,GPU busy 3.6 ms of it.
The convention, unchanged, with citro3d as the API. Line counts are the 09-07 snapshot; costs are Old 3DS hardware probes from the fork. The New 3DS reached a 59.6 fps median only by running this a frame ahead on a second core.

Where we ended up — v1.1.0

The game still decides what to draw; the native renderer replaces how it reaches the GPU, one category at a time.

V1.1.0 DEFAULTS · OLD AND NEW 3DSNATIVE RACE RENDERER (NRR)TrackBaked once percourseMachinesRecorded once,replayedSkyState learnedonceHUDMemoisedprogramsFX · shadows ·scenerySprite batchesDecorationsRecorded atfirst sightMenus, previews, the walk between markersCaptured once, replayed as compiled programs (ni_*).Everything elseOur own F3DEX2 walker: credits, editors, transitions, framesetup.libultraship's interpreterOnly with [perf] preset = off.The gameStill runs its owndraw code everyframe. Patches swap arace's heavy sectionsfor a marker plus aper-tick snapshot(matrices, LODs,colours).racePICA200 command bufferPrecomputed register wordswritten straight in. Trackand machine verticestransformed on the GPU.3D: one submissionBody built once; the GPUdraws it for the left eye,jumps, and draws it againfor the right.3D onBottom screenDASH and bottom menus: the CPU paints a 320×240 buffer andcopies changed columns. No GPU.
V1.1.0 DEFAULTSThe gameStill runs its own draw code everyframe. Patches swap a race's heavysections for a marker plus a per-ticksnapshot (matrices, LODs, colours).Native race renderer (NRR)Track: baked once per course;Machines: recorded once, replayed;Sky: state learned once; HUD:memoised programs; FX · shadows ·scenery: sprite batches;Decorations: recorded at firstsight.race sectionsMenus, previews, the walkbetween markersCaptured once, replayed as compiledprograms (ni_*).menusEverything elseOur own F3DEX2 walker: credits,editors, transitions, frame setup.the restlibultraship's interpreterOnly with [perf] preset = off.escape hatchBottom screenDASH and bottom menus: the CPUpaints a 320×240 buffer and copieschanged columns. No GPU.bottom screenPICA200 command bufferPrecomputed register words writtenstraight in. Track and machinevertices transformed on the GPU.3D: one submissionBody built once; the GPU draws it forthe left eye, jumps, and draws itagain for the right.
The game still decides what is drawn, so the pictures match. What changed is who turns that into GPU work: for what a race spends its frame on, a small path per category writes the PICA200's own command words.

Why fork the renderer

On 09-07 the 3DS build compiled 36 libultraship files, 15,043 lines, dominated by the Fast3D interpreter (10,466 lines), plus a 12,353-line bridge that only widens the game's 32-bit display-list words to 64 bits. Everything else (window, audio, input, config) was already the port's own code. A census found machines, track and effects each drawn differently, so the fork is a hybrid: our own interpreter for everything, plus race-only paths for what a race frame spends its time on. The long tail (editors, ending, one-off screens) stays interpreted.

TierWhat it doesAt release
1 · Own the interpreterA purpose-built F3DEX2 → citro3d walker reads the game's display list directly. No bridge.Default on both consoles; [perf] preset = off restores the libultraship path.
2 · Bypass it for racesTrack, machines, sky, HUD, effects and shadows drawn from baked or recorded data: the native race renderer (NRR).Default on both consoles; since v1.1.0 it also covers menus, course select and machine select.

Every lever is an ini killswitch with a verify mode that draws both paths and compares the vertex bytes bit for bit. [perf] preset = auto resolves per console at startup (n60 on New 3DS, o3ds-auto on Old 3DS) and turns the whole set on; an explicit ini key always wins.

The renderer work waited two weeks for the dev loop: a network listener that accepts a build, writes it to SD and relaunches through the Homebrew Launcher, and a live log link over 3dslink's protocol. One Y-press per power-on; after that the console reloaded builds on its own.

Case by case

Each part of a frame at v1.1.0 defaults. The last column is Old 3DS CPU per rendered 2D race frame: the 10-01 interpreter build against native-renderer card 3 the next day.

CategoryBefore (v0.9.0)Shipped path (v1.1.0 defaults)Old 3DS ms
MachinesAll 30 racers rebuilt as display-list commands and transformed on the CPU every frame.A machine's first ordinary draw is its recording: from the next frame it is a retained mesh drawn as one GPU-transformed draw per material run, fed by a snapshot of its matrix, LOD and colours. First sight, crashed machines and time-attack ghosts stay on the ordinary path.4.05 → 1.09
TrackThe course tessellator re-wrote every ring's vertices each frame; the interpreter transformed them again.Every distinct ring is baked once per course load into a GPU buffer, inside one arena reserved at boot (no heap fragmentation across loads). The game's own chunk cull and walk still choose what is drawn; each frame only writes indices, from 18 index templates. The GPU adds an exact integer eye offset.4.13 → 1.35
Sky and backdropWalked and transformed like everything else.The backdrop's state blocks are learned once; each frame the CPU transforms a handful of snapshot vertices and draws one packed draw per state block.3.02 → 0.40
HUD (top screen)One draw per texture rectangle, colour changes splitting batches.The game's HUD code still runs; its commands are compacted behind markers and replayed as memoised, compiled register programs. About 12 of 14 runs are byte-identical from frame to frame.5.14 → 3.17
Effects, shadows, scenerySprite by sprite through the interpreter.One shared engine: state learned once, then the per-tick snapshot drawn as sprite batches. Course decorations use the machines' record-at-first-sight pattern.3.37 → 1.72
FogThe PICA fog unit, plus a combiner stage for the game's shade-alpha fog.The NRR switches the fog unit off and blends fog in a combiner stage with the colour written as a constant; a track run that needs fog it cannot express falls back.
MenusEvery non-race screen walked by the interpreter.Since v1.1.0, menus, course previews and machine select are captured once and replayed as compiled programs (ni_*). The spiral transitions and one outline pass still go through the walker.
Bottom screenA debug console.DASH and the bottom-screen menus never touch the GPU: the CPU paints a 320×240 buffer and copies only the changed columns.
Stereo 3DEach draw issued twice, once per eye.The frame body is emitted once; a per-draw uniform carries the eye shift. Since v1.1.0 both eyes go in one GPU submission: the GPU runs the body, jumps to a right-eye prologue, runs it again. Old 3DS 3D menu 40.2 → 44.6 fps.
Still on an interpreterEverything.Our own F3DEX2 walker (Tier 1) handles the frame setup between markers, credits, editors, demos and transitions. libultraship's interpreter runs only under preset = off.0.82 → 0.86

Part 2 · The Old 3DS

The Old 3DS road

The 08-11 feasibility study said the port could not fit an Old 3DS (268 MHz, no L2 cache, 64 MB). It booted there on 09-29 at about 10 fps, raced at 60 in 2D on 10-02 and in 3D on 10-08. Every card was a physical SD-card trip.

Old 3DS, 10-26NowMeasured onBefore
1P race, 2D60 fps, median frame 16.5 ms, 0 hitchescard 3, 10-02~10 fps, 09-29
1P race, 3D59.7–59.8 fps with one submission (suggestive)v1.1 card 6, 10-2656.4 / 59.3 baseline; 53.7 with 16 machines (10-24)
Menus, 3D44.6 fpsv1.1 card 6, 10-2640.2
Multiplayer2D 60 fps (2 players + 14 CPU); 3D locked 30 by designv1.1 card 3, 10-25; 10-24 probenew in v1.1.0

Every card, on the calendar

A card is one round trip: build, ini, test plan, play session, log back. Hover or tap a card for what it carried.

60 fps reachedran, below 60crash / regressionmeasurement / probeother log set on filepause, no cards

September 2026

Mon
Tue
Wed
Thu
Fri
Sat
Sun
21
22
23
24milestone reopens
25fits (sim)
26
27
28v0.9 ✗ OOM09-28 · Shipped build (RDRAM 16)The released New 3DS build, unchanged, installed as a CIA
Dies in the boot preload: [fatal] operator new failed size=175844 heapFree=32424. Console answers memtype=0, heap grant 24 MiB (25,165,824 B)hw-first-boot-0928
29boot · 10 fps09-29 · First boot (RDRAM 8)-DGDX_3DS_RDRAM_MB=8, audio_stream=1, tex_upload_kb=512; legacy interpreter (native all=0)
Boots and races. Wall p50 96.0 ms (p10 88.7, p90 111.3); GPU busy 3.6 ms; oth 37.3 ms. User: "it ran ! at like 9fps"hw-rdram8-0929
run 1 · 1109-29 · C4 run 1: O3DS profile-DGDX_3DS_O3DS=ON (RDRAM 8 MiB + 16 MiB linear pin: 38.9 MB heap), owned interpreter all=1 interp=1
GP wall 88 ms (11.3 fps), TT/menu 43.7 ms; aud 25.8 ms = audio-lock wait, confirmed on hardwarehw-c4run1-0929
run 2 · 1509-29 · C4 run 2: narrow audio lockaudio_ctx_narrow=1
aud 25.8 → 0.2 ms; race wall 67.8 ms (14.7 fps), light scenes 40 mshw-c4run2-0929
HOME / SIMD09-29 · Wave 2: HOME + audio SIMDHOME bisect builds; audio_simd verify
HOME freeze root cause isolated (bottom backlight session); 7/7 HOME resumes with screen on; audio_simd verify 391/0/0, 0 mismatches on siliconhw-wave2-0929
skip · 19–2809-29 · Wave 3: frameskip + O3DS presetframeskip=auto, preset=o3ds, gpuxf=1, DrawProbe calibration CIA
Logic 57–60 ticks/s (game at full speed for the first time); drawn 19–28 fps in races. User: "playable but nausea inducing". DrawProbe: 10.9 µs CPU per draw, 14 Mtri/shw-wave3-0929
REL1 ✗ boot09-29 · REL1All C5 stage-1 levers + O3DS code generation (cold -Os, hot-first order)
Boot crash, deterministic. Four-title bisect: cold -Os crashes, stock and -O2+order boothw-rel1-fail, hw-rel1b-fail, hw-rel1crash-bisect
30REL2 · 1509-30 · REL2Stock codegen, frameskip=steady, HUD/machine/track retained paths
Boots. Race ~15 fps rendered (steady stuck at D=4), rw50 26–35 ms, "lots of awkward hitching": synchronous SD log flushes of 26–270 ms on mainhw-rel2
REL3 · 2009-30 · REL3Every exact lever on, async file log, overlap kick
"no hitches, steadier"; steady mostly D=3 (20 fps rendered), rw50 24–36 mshw-rel3-run1, hw-rel3-run2
PMU · CPI 4.309-30 · HWPROF BPMU counters (c5pmu) and the r60 probe
Render CPI 4.34, 43.6% of cycles in instruction-fetch stalls; probe CPI 1.06 at 16 KB of hot code, 4.10 at 24 KB, 5.62 at 32 KBhw-hwprof-B

October 2026

Mon
Tue
Wed
Thu
Fri
Sat
Sun
1REL4 · replay10-01 · REL4+ pose replay (C6), compact HUD/sky emitter
20 rendered + 20 replayed images/s, 1.3 ms per replay, but sky jitter, black tearing edges, giant fx triangles. 26.8 ms CPU per rendered frame. User: "we should go for a real 60!"hw-rel4
NRR ✗ freeze10-01 · n60-m1 (NRR milestone 1)Native race renderer: HUD, track, machines, sky; nrr_direct=2 verify run
Hard freeze at race start. Cause: verify mode's state save/restore rolled back libctru's live GX queue (the emulator's GSP completes synchronously and cannot show it)hw-n60m1-V
2card 2 · 5710-02 · n60-card2All seven NRR categories (+ shadows, fx, dressing), GX-queue fix
RUN V: 199,472 draws verified, bad=0, float24 max 0.011 px. RUN C: TT "silk 60 fps"; GP W median 14.2 ms = 57.3 fps outside log-flush windows (51.9 fps all windows), harsh hitch every ~1/5 laphw-n60card2-V, hw-n60card2-C
card 3 · 60 2D10-02 · n60-card3Never-blocking log, frameskip=race0, save fix, swap gate, texture fixes, HUD memo, native tunnel joins and decorations
Race median 16.5 ms, 0 race hitches; save persisted across a power cycle. User: "omg it ran amazing!"hw-n60card3
33D ✗ sky10-03 · interim legacy 3Dcard-3 build + [stereo] enabled=1 (NRR refused stereo, legacy path)
Black sky: the NRR sky consumed the section but could not draw under stereohw-interim3d
3D · 4010-03 · n60-3dStereo on the NRR: one frame body, resubmitted for the right eye with a per-eye uniform
No glitches; 3D race ~19.8 ms vs 16.5 ms 2D (frame-begin GPU wait 6.6 vs 3.3 ms) → 33 ms quantum, cost guard flapping, ~40 fpshw-n60-3d
3D · 48–5710-03 · n60-3d2Right eye interleaved per submitted chunk
2D 60 in GP and TT. 3D "40s and 50s": every race tripped the guard in the intro; steady 3D GP ≈ 48–57 fpshw-n60-3d2
4rate limit
5
6
73D · ~60 MC10-07 · n60-3d3End-mode swap pacing in 3D, intro HUD fix, 1 Hz menu refresh fix, launch variants
Mute City 3D ~60 in end mode (hold 1 on 250–255 of 256 frames); Silence and Sand Ocean heavier (work 16–18 ms)hw-n60-3d3
8✗ VRAM10-08 · n60-3d4+ track merge, per-draw diet, prewarm, HUD compile, FINISH mesh, lcd_vram=1
Crash at boot on all three variants, no log: LCD framebuffers in VRAM, which the CIA maps read-only for the CPUhw-n60-3d4
card 4 · 3D 6010-08 · card 4 (VRAM fix)Same build, two levers off in the SD ini
3D GP courses 12–16: race windows hold 1 on 245–256 of 256 frames at work ≈15.5 ms → ≈59–60 fps. VERIFY variant bad=0. Heavy windows only on the post-race results tablehw-n60-3d4b1
9rate limit
10
11
12
13
14
15
16
17
18
19v1.0.0
20v1.1.0 work
21
2251610-22 · o3ds-516Old 3DS hardware log set from the v1.1.0 tuning work; not analysed on this pagehwlogs/o3ds-516185910-22 · o3ds-1859Old 3DS hardware log set from the v1.1.0 tuning work; not analysed on this pagehwlogs/o3ds-1859b5f10-22 · o3ds-b5fOld 3DS hardware log set from the v1.1.0 tuning work; not analysed on this pagehwlogs/o3ds-b5f
2390010-23 · o3ds-900Old 3DS hardware log set from the v1.1.0 tuning work; not analysed on this pagehwlogs/o3ds-9004bf10-23 · o3ds-4bfOld 3DS hardware log set from the v1.1.0 tuning work; not analysed on this pagehwlogs/o3ds-4bf15810-23 · o3ds-158Old 3DS hardware log set from the v1.1.0 tuning work; not analysed on this pagehwlogs/o3ds-1585c210-23 · o3ds-5c2Old 3DS hardware log set from the v1.1.0 tuning work; not analysed on this pagehwlogs/o3ds-5c2d9d10-23 · o3ds-d9dOld 3DS hardware log set from the v1.1.0 tuning work; not analysed on this pagehwlogs/o3ds-d9d52a10-23 · o3ds-52aOld 3DS hardware log set from the v1.1.0 tuning work; not analysed on this pagehwlogs/o3ds-52a
243D · 54 · CPU-bound10-24 · o3ds-3d ceiling probe (0c2668a)Lockstep 3D race in D=1 blocks, plain vs the previous frame's GPU work waited out first
Idling the GPU first did not make the frame cheaper (13.5 → 14.2 ms, 11.8 → 12.3 ms): Old 3DS 3D is CPU-bound; the deferred-wait plan was dropped. 1P 3D race with 16 machines: wall 18.6 ms = 53.7 fps. 3D multiplayer locked to 30 (ed04b17)hwlogs/o3ds-0c2; cpu-profile-answer.md; ed04b17
a3b10-24 · o3ds-a3bOld 3DS hardware log set from the v1.1.0 tuning work; not analysed on this pagehwlogs/o3ds-a3b90810-24 · o3ds-908Old 3DS hardware log set from the v1.1.0 tuning work; not analysed on this pagehwlogs/o3ds-908b5210-24 · o3ds-b52Old 3DS hardware log set from the v1.1.0 tuning work; not analysed on this pagehwlogs/o3ds-b52
25MP 2D · 6010-25 · v1.1 card 3 (3c0c7b7)Online 2D race, 2 players + 14 CPU machines
Race windows at a locked 60 (wall 16.71 ms, 3.8–4.1 ms of vblank slack); the steady governor still flapped 60/30 at D=1, fixed by the drift rule (a89289f)hwlogs/o3ds-card3; cpu-profile-answer.md §A.4
online110-25 · o3ds-online1Old 3DS hardware log set from the v1.1.0 tuning work; not analysed on this pagehwlogs/o3ds-online1card210-25 · o3ds-card2Old 3DS hardware log set from the v1.1.0 tuning work; not analysed on this pagehwlogs/o3ds-card2card2b10-25 · o3ds-card2bOld 3DS hardware log set from the v1.1.0 tuning work; not analysed on this pagehwlogs/o3ds-card2bcard2c10-25 · o3ds-card2cOld 3DS hardware log set from the v1.1.0 tuning work; not analysed on this pagehwlogs/o3ds-card2c
26v1.1.0✗ HOME close10-26 · v1.1 card 5 (5e19320)Expansion Kit test build, v1.1.0 work
HOME-close hard lock (a 73 KB allocation in the 64DD save flush)hwlogs/o3ds-card5
card 6 · 3D 6010-26 · v1.1 card 6 (ba0692e)One GX submission for both eyes, four-arm automatic A/B; heap fixes
1P 3D race 59.7–59.8 fps with one submission (baseline 56.4 / 59.3; two blocks per arm, so suggestive); 3D menu 40.2 → 44.6 fps; HOME close cleanhwlogs/o3ds-card6; onesub-hw/o3ds-card6-analysis.md
27
28
29
30
31
  1. 09-28 Shipped build (RDRAM 16) — Dies in the boot preload: [fatal] operator new failed size=175844 heapFree=32424. Console answers memtype=0, heap grant 24 MiB (25,165,824 B)
  2. 09-29 First boot (RDRAM 8) — Boots and races. Wall p50 96.0 ms (p10 88.7, p90 111.3); GPU busy 3.6 ms; oth 37.3 ms. User: "it ran ! at like 9fps"
  3. 09-29 C4 run 1: O3DS profile — GP wall 88 ms (11.3 fps), TT/menu 43.7 ms; aud 25.8 ms = audio-lock wait, confirmed on hardware
  4. 09-29 C4 run 2: narrow audio lock — aud 25.8 → 0.2 ms; race wall 67.8 ms (14.7 fps), light scenes 40 ms
  5. 09-29 Wave 2: HOME + audio SIMD — HOME freeze root cause isolated (bottom backlight session); 7/7 HOME resumes with screen on; audio_simd verify 391/0/0, 0 mismatches on silicon
  6. 09-29 Wave 3: frameskip + O3DS preset — Logic 57–60 ticks/s (game at full speed for the first time); drawn 19–28 fps in races. User: "playable but nausea inducing". DrawProbe: 10.9 µs CPU per draw, 14 Mtri/s
  7. 09-29 REL1 — Boot crash, deterministic. Four-title bisect: cold -Os crashes, stock and -O2+order boot
  8. 09-30 REL2 — Boots. Race ~15 fps rendered (steady stuck at D=4), rw50 26–35 ms, "lots of awkward hitching": synchronous SD log flushes of 26–270 ms on main
  9. 09-30 REL3 — "no hitches, steadier"; steady mostly D=3 (20 fps rendered), rw50 24–36 ms
  10. 09-30 HWPROF B — Render CPI 4.34, 43.6% of cycles in instruction-fetch stalls; probe CPI 1.06 at 16 KB of hot code, 4.10 at 24 KB, 5.62 at 32 KB
  11. 10-01 REL4 — 20 rendered + 20 replayed images/s, 1.3 ms per replay, but sky jitter, black tearing edges, giant fx triangles. 26.8 ms CPU per rendered frame. User: "we should go for a real 60!"
  12. 10-01 n60-m1 (NRR milestone 1) — Hard freeze at race start. Cause: verify mode's state save/restore rolled back libctru's live GX queue (the emulator's GSP completes synchronously and cannot show it)
  13. 10-02 n60-card2 — RUN V: 199,472 draws verified, bad=0, float24 max 0.011 px. RUN C: TT "silk 60 fps"; GP W median 14.2 ms = 57.3 fps outside log-flush windows (51.9 fps all windows), harsh hitch every ~1/5 lap
  14. 10-02 n60-card3 — Race median 16.5 ms, 0 race hitches; save persisted across a power cycle. User: "omg it ran amazing!"
  15. 10-03 interim legacy 3D — Black sky: the NRR sky consumed the section but could not draw under stereo
  16. 10-03 n60-3d — No glitches; 3D race ~19.8 ms vs 16.5 ms 2D (frame-begin GPU wait 6.6 vs 3.3 ms) → 33 ms quantum, cost guard flapping, ~40 fps
  17. 10-03 n60-3d2 — 2D 60 in GP and TT. 3D "40s and 50s": every race tripped the guard in the intro; steady 3D GP ≈ 48–57 fps
  18. 10-07 n60-3d3 — Mute City 3D ~60 in end mode (hold 1 on 250–255 of 256 frames); Silence and Sand Ocean heavier (work 16–18 ms)
  19. 10-08 n60-3d4 — Crash at boot on all three variants, no log: LCD framebuffers in VRAM, which the CIA maps read-only for the CPU
  20. 10-08 card 4 (VRAM fix) — 3D GP courses 12–16: race windows hold 1 on 245–256 of 256 frames at work ≈15.5 ms → ≈59–60 fps. VERIFY variant bad=0. Heavy windows only on the post-race results table
  21. 10-24 o3ds-3d ceiling probe (0c2668a) — Idling the GPU first did not make the frame cheaper (13.5 → 14.2 ms, 11.8 → 12.3 ms): Old 3DS 3D is CPU-bound; the deferred-wait plan was dropped. 1P 3D race with 16 machines: wall 18.6 ms = 53.7 fps. 3D multiplayer locked to 30 (ed04b17)
  22. 10-25 v1.1 card 3 (3c0c7b7) — Race windows at a locked 60 (wall 16.71 ms, 3.8–4.1 ms of vblank slack); the steady governor still flapped 60/30 at D=1, fixed by the drift rule (a89289f)
  23. 10-26 v1.1 card 5 (5e19320) — HOME-close hard lock (a 73 KB allocation in the 64DD save flush)
  24. 10-26 v1.1 card 6 (ba0692e) — 1P 3D race 59.7–59.8 fps with one submission (baseline 56.4 / 59.3; two blocks per arm, so suggestive); 3D menu 40.2 → 44.6 fps; HOME close clean
2D, 1P3D, 1P2D multiplayer
pausepause020406009-2910-0110-0810-1510-1910-2210-26v1.0.009-29 · first boot: 10.4 fps09-29 · audio lock: 14.7 fps09-30 · REL2: 15 fps09-30 · REL3: 20 fps10-01 · REL4: 20 fps10-02 · card 2: 57.3 fps10-02 · card 3: 60 fps10-03 · 3D first: 40 fps10-03 · 3D chunked: 52 fps (estimate)10-07 · Mute City: 60 fps (estimate)10-08 · card 4: 59.5 fps10-24 · 10-24 1P 3D: 53.7 fps10-25 · MP 2D, 16: 60 fps10-26 · card 6: 59.7 fpsfirst bootREL3card 33D firstcard 410-24 1P 3Dcard 6race frames per second
Old 3DS race frame rate over time. One dot per figure a hardware log named. Hollow dots are estimates from documented ranges. Before the native race renderer the game also ran in slow motion. The 10-24 dip is a heavier 16-machine measurement, not a regression.

The machine

Old 3DSNew 3DS
CPU2 × ARM11, 268 MHz, no L24 × ARM11, up to 804 MHz, 2 MB L2
L1 instruction cache16 KB; a miss costs ~37 cycles (measured)misses can hit L2
App memory64 MB region, 24 MiB heap grant124 MB
Usable corescore 0; core 1 is a 30% share (audio)core 0 + core 2

Five phases

1 · Make it fit (09-24 → 09-29). RDRAM arena halved to 8 MiB, audio_table streamed; replay 49/49 with 2.65 MiB spare under simulation, and the console matched it byte for byte. One flag, -DGDX_3DS_O3DS=ON, makes the Old 3DS build.

2 · Find the costs (09-29 → 09-30). First race: 96 ms a frame, GPU busy 3.6 ms. The audio lock wait went 25.8 → 0.2 ms, ARMv6 SIMD made synthesis 2.2× cheaper, and frame skipping fixed the slow motion ("playable but nausea inducing"). A retained renderer won in the emulator and did almost nothing on hardware.

3 · The probe that changed the plan (09-30 → 10-01). Performance counters: CPI 4.34, 43.6% of cycles stalled on instruction fetch. A probe loop ran at one instruction per cycle with 16 KB of hot code and four at 24 KB. Research called native 60 "marginal" and offered pose replay; the user: "we should go for a real 60!"

024616 KB · CPI: 1.061.0616 KB24 KB · CPI: 4.104.1024 KB32 KB · CPI: 5.625.6232 KBcycles per instruction
The I-cache cliff, Old 3DS hardware. One loop at three hot-code sizes.

4 · The native race renderer (10-01 → 10-02). One small loop per race category, each sized to the 16 KB L1-I, writing command words from GPU-resident meshes. Game logic untouched and hash-checked every tick.

before, 10-01card 3, 10-02
01346hud · interpreter + early levers: 5.145.14hud · card 3 (native renderer): 3.173.17hudtrk · interpreter + early levers: 4.134.13trk · card 3 (native renderer): 1.351.35trkmach+mesh · interpreter + early levers: 4.054.05mach+mesh · card 3 (native renderer): 1.091.09mach+meshsky · interpreter + early levers: 3.023.02sky · card 3 (native renderer): 0.400.40skydress · interpreter + early levers: 1.711.71dress · card 3 (native renderer): 0.810.81dressfx · interpreter + early levers: 1.161.16fx · card 3 (native renderer): 0.640.64fxshd · interpreter + early levers: 0.500.50shd · card 3 (native renderer): 0.270.27shdmisc · interpreter + early levers: 0.820.82misc · card 3 (native renderer): 0.860.86miscfend · interpreter + early levers: 0.560.56fend · card 3 (native renderer): 0.460.46fendms per rendered frame, hardware
CPU per rendered frame by category, 10-01 vs card 3 (10-02). Sum 21.1 → 9.1 ms; logic per tick 1.51 → 1.58 ms, unchanged by design.

5 · 3D (10-03 → 10-08). The frame body is built once and resubmitted for the right eye with a per-eye uniform. The CPU fit, but frames missed vblank behind a 5–7 ms right-eye tail; interleaving per chunk and end-mode swaps gave Grand Prix windows at 59–60 on 10-08.

Later (10-24 → 10-26): a heavier 16-machine race measured 53.7 fps and idling the GPU first did not help, so 3D here is CPU-bound and multiplayer 3D is locked to 30. One submission (below) raised the 1P race to 59.7–59.8. — hwlogs/o3ds-0c2, o3ds-card6; ed04b17

One submission for both eyes (v1.1.0)

After v1.0.0, a 3DS homebrew developer asked in a public comment whether the stereo path submits each eye separately, and suggested one command list for both. It did not re-encode the right eye, but every replayed list was its own GX queue entry, with its own GSP submit, completion interrupt and event-thread wake on core 0. Old 3DS probes put a clean citro3d draw at 10.9 µs and showed 0.4–0.5 ms of GPU dead time per backlogged queue entry (about 40 µs on New 3DS).

The fix uses the PICA200's second command-buffer channel: each stereo chunk ends by jumping to a short right-eye prologue, which jumps back into the same chunk, then to the finalize words. GSP gets one entry; the GPU runs the body twice and raises one interrupt. In the emulator every frame hash, game-state hash and screenshot matched the old path. On hardware, one build per console cycled four arms automatically:

GX entriescompletion IRQs
03690 baseline · GX entries / frame: 990 baseline · completion IRQs / frame: 550 baseline1 one submit · GX entries / frame: 771 one submit · completion IRQs / frame: 331 one submit2 + no kick · GX entries / frame: 662 + no kick · completion IRQs / frame: 222 + no kick3 + narrow flush · GX entries / frame: 773 + narrow flush · completion IRQs / frame: 333 + narrow flushper frame, Old 3DS, 1P 3D race
Old 3DS, card 6 (10-26). GX queue entries and completion interrupts per frame, per arm, from the [gxcost] receipts.
Old 3DS0 baseline1 one submit2 + no kick3 + narrow flush
3D menu, static screen40.2 fps44.644.644.6
1P 3D race (2 blocks per arm)56.4 / 59.359.7–59.859.7–59.859.7–59.8
Headroom on the critical path–≈ 1.5 ms≈ 0≈ 1.8 ms

The menu is the clean result (6 against 9 blocks, ±0.2 fps); the race line is suggestive. Dropping the mid-frame kick saved CPU but lost CPU/GPU overlap, so it netted zero. The New 3DS was neutral. Arm 3 ships as the v1.1.0 default on both consoles ([stereo] onesubmit = 1, onesubmit_narrow = 1).

Memory

The Old 3DS margin was thin at v1.0.0 and stayed the main source of post-release bugs:

  • HOME close hard-locked the console (card 5). The 64DD save flush at exit wanted one contiguous 73 KB buffer; about 390 KB was free, but not in one piece. bad_alloc led to svcBreak, which locks instead of exiting. The flush now streams with no heap allocation, and a break during teardown exits. Card 6 logged a clean exit.
  • The fast track path dropped after six course loads. A 96,768-byte allocation failed with 1.6 MB free: fragmentation, not exhaustion. The track now lives in one 777,360-byte arena reserved at boot (emulator-verified over a 6-course Grand Prix).
  • About 320 KB moved off the main heap, mostly the bottom screen's art buffers, which had been static arrays in the program image.

What the Old 3DS forced

  • Leaving the display list in races. A faster interpreter on a second core was enough on the New 3DS; at a third of the clock with no L2 and no spare core, the interpreter's own code did not fit in cache.
  • A hardware profiler. The emulator models neither the I-cache nor the console's memory permissions, so its wins stopped predicting hardware. ARM11 performance counters became the judge.
  • A memory diet. A smaller RDRAM arena, streamed audio tables, capped uploads, a pinned heap layout, and a high-water mark instead of a free-list figure that had been misread as headroom.
  • Frame pacing as design. With a millisecond or two of margin, when the swap is requested matters: a swap gate, an end-of-frame mode, and a race-scoped frame-skip guard.

Still open

  • Heavier venues (Silence, Sand Ocean) can step 3D down to 30.
  • The results table after a race is still heavy.
  • Brief audio gaps during loads. The vblank-rate crackle theory was measured and refuted.
  • Memory remains a listed known issue.

Part 3 · What shipped

The Expansion Kit

The 64DD Expansion Kit (course editor, machine creator, disk content) is its own build, "G-Diffuser EK", with its own HOME-menu title, save and log, so it installs beside the plain game. It needs the player's own retail Japanese 64DD disk image, streamed from SD rather than held in RAM (62 MB saved); without one, the EK entries simply don't open. A fan-translated English image froze at boot in testing and is a known issue.

Under the Old 3DS profile in the emulator it fits with ~6 MB spare and matches the plain game tick for tick (6,002/6,002 on a ROM course, 8,601/8,601 on a disk course). On Old 3DS hardware disk courses stuttered, disk music broke up and the editors crawled, so it ships for the New 3DS only.

The bottom screen: DASH

The second screen started as a debug console and became a race dashboard: lap, position, energy, splits and the map, painted by the CPU straight into the bottom framebuffer, no GPU. Three layouts (all top, split, all bottom) cycle from a touch chip or a SELECT long-press. On Old 3DS hardware it cost 4.2–5.6 ms a frame, so it was put on a 1 ms budget. In 2–4 player races the HUD stays on the top screen. A drag-and-resize layout editor was shelved; a one-frame speed spike (up to ~826 KPH) at the finish line is known and cosmetic.

The New 3DS freeze

A real New 3DS froze, with no dump and no log line, every time the attract demo reached its full 30-machine race; the emulator never reproduced it. Bisection on the console found the GPU command buffer sized at the edge of that frame: with one lever off, the same frame hit a bounds-checked panic in the buffer writer instead. The buffer went from 2× to 4× the citro3d default on both consoles, with a log line before any future overflow. Four hardware boots through that frame came back clean.

Multiplayer

Most of v1.1.0: 2–4 players over local wireless, the same Wi-Fi, or online with a room code, each console showing its own racer, CPU machines filling the field. Old and New 3DS can share a room. On the Old 3DS, multiplayer races run in 2D at 60 by default; 3D is locked to 30. Player guide: MULTIPLAYER.md.

The MULTIPLAYER menu on the bottom screen: PLAY NEARBY and PLAY ONLINE; Select Mode with VS on the top screen
VS opens MULTIPLAYER on the bottom screen.
The ROOM CODE keypad on the bottom screen with four of five letters typed
Joining an online room with the host's 5-letter code.
Select Machine on the top screen; the room panel on the bottom screen with every player picking and a round timer
Machine select, with every player's pick on the bottom screen.
Race countdown with two other players' machines alongside; the DASH lists all four players' standings
The countdown: each console shows its own racer.

Emulator captures (Azahar), not hardware: four New 3DS instances in an online room through a local rendezvous server.

Release: v1.0.0, then v1.1.0

v1.0.0 (10-19) is three downloads from one tree: Old 3DS, New 3DS, and New 3DS with the Expansion Kit. All share one SD folder and pick their preset at startup. v1.1.0 (10-26) keeps the same three downloads and adds multiplayer, native menus and one-submission 3D. See RELEASE-NOTES.md and STATUS.md.

Known issue (v1.0.0, still open)What it means
3D is not a locked 60 on either consoleOld 3DS 3D is CPU-bound: 59–60 on most courses, some step to 30; multiplayer 3D is 30
The Old 3DS memory ceilingA large mid-race asset load can exhaust the budget in a worst-case emulator run; hardware peaks near 24.2 MB
Brief audio gaps during loadsShort and occasional
A speed-readout spike at the finishCosmetic, DASH only
Not every mode tested on every modelCoverage favours the modes used most in development

The day-by-day account is on Every step; the hardest fights on Difficulties.

06 · Pushing the boundaries

Pushing the boundaries

How this port compares with other N64 projects on the 3DS, from their own READMEs and press, each claim linked. Searched on 2026-10-08, still current at release. None of their figures were measured by us, and the games differ in load, so this is a comparison of claims, not a benchmark.

The field

ProjectSourceApproachWhat it says about Old / New 3DS performanceStereo 3D
G-Diffuser 3DS (this project)The road to 60, all over againF-Zero X decomp on libultraship; own F3DEX2 walker plus a native race renderer writing PICA200 command wordsOld 3DS: 2D races at 60 (median frame 16.5 ms); 3D 59–60 on most courses, CPU-bound, some step to 30. New 3DS: 3D races at 60 in every window. Native 60 Hz logic, no interpolation (our hardware logs)Yes
Super Mario 64 3DS port (mkst)mkst/sm64-port, branch 3ds-portDecomp PC port; renderer details not in the READMEREADME: "Naïve frame-skip if frame takes longer than 33.3ms"; a 60 FPS enhancement patch is listed. Press (2020): "a solid 30fps", model not stated (VGC)Yes (README)
Ocarina of Time 64 3DS portAndresac90/OOT-64-3DS-Portzeldaret/oot decomp; renderer started from the SM64 PC port's Fast3D rendererREADME: New 3DS "60 in 2D and 3D" by interpolation (measured on hardware). Old 3DS: "about 15–17 in the biggest scene (Kokiri Forest), around 45 in interiors" (measured at simulated Old 3DS speed); "60 everywhere is still being worked on"Yes
Majora's Mask 64 3DS portAndresac90/MM-64-3DS-Portdecomp"Nothing runs on the 3DS yet"–
Smash 64 for New 3DS2gifts/smash64-3dsdecomp; credits the SM64 3DS graphics backend, citro3d; renders on the New 3DS's third core"Original 3DS/2DS models are not supported"; "a locked 60 FPS is not guaranteed in every situation"Yes
Mario Kart 64 3DSEstebanPdN/mario-kart-64-3dsSpaghettiKart (libultraship) based; Fast3D vertex/texture ranges, combiner plans compiled once per material"retains its original 30 Hz game simulation"; an adaptive interpolated midpoint frame on Old and New 3DS, "not a promise of a fixed or sustained 60 FPS mode"Not mentioned
Ship of Harkinian 3DS999sian/soh-3dslibultraship (SoH); PICA200 renderer on citro3d, built for F3DEX2New 3DS is the main target; "Old 3DS uses native 20 FPS presentation"; "Physical Old 3DS gameplay is not yet verified"Not mentioned
rt64-3dsoneup03/rt64-3dsRenderer for N64ModernRuntime static recompilations: F3DEX2 interpreter, CPU vertex transform and lighting, per-eye replay"Under construction. First target is Donkey Kong 64 (New 3DS only)"; no frame ratesYes
sm64coopdx New 3DSPR #26citro3dNot yet called playable; "30 FPS stability" listed as a next gate–
DaedalusX64-3DS (emulator)GameBrew, Wololo 2020HLE emulator, ARMv6 dynarec"Tested on N3DS, runs about at 20-30FPS on average" (v1.0, 2022-10-16)Not verified
Ocarina of Time 3D (Nintendo/Grezzo, 2011)Nintendo LifeRemake, native 3DS engineMiyamoto: "It ran at 20fps on the Nintendo64, but on the 3DS, it runs at 30fps"Yes
Star Fox 64 3D (2011)Nintendo Life reviewRemakeNumeric frame rate: not foundYes

What we can and cannot say

  • We found no other N64 decompilation, recompilation or emulator project on the 3DS that claims a sustained 60 fps on an Old 3DS. The nearest are the OoT port (15–45 fps by scene, measured at simulated Old 3DS speed), the MK64 port (adaptive interpolation over 30 Hz logic), SoH-3DS (20 fps presentation, unverified on hardware) and the SM64 port.
  • Every renderer we found interprets N64 display lists (Fast3D / F3DEX2 translated to citro3d). We found none that, like this project's native renderer, skips display-list construction for whole categories of geometry and writes precomputed PICA200 command words instead.
  • Our 60 was race-only at v1.0.0: menus ran on the interpreter with frame skipping. Since v1.1.0 the menus, transitions, course select and machine select draw natively too; the post-race results table is still heavy. Several of the projects above target the whole game and interpolate instead; that is a different trade, not a lesser one.
  • No other native F-Zero X port on the 3DS was found. Wololo (2020), citing a Reddit compatibility list, names F-Zero X among the games with "playable performance" on the New 3DS under DaedalusX64.
  • Not verified: Old 3DS frame rates for the SM64 port and DaedalusX64 (the sources are forum threads that returned HTTP 403), stereo support in DaedalusX64, and a numeric frame rate for Star Fox 64 3D.
  • Also searched and not found: a Star Fox 64 (Starship), Perfect Dark, Banjo or 2Ship 3DS port; an upstream libultraship 3DS backend; a Mupen64Plus or ParaLLEl-N64 core for 3DS.
  • Correction to this post-mortem's own Frontier page (written 09-03): it describes the SM64 3DS port as 30 Hz logic interpolated to 60. The README read for this page describes naive frame skipping below 30 fps and lists a separate 60 FPS patch; we could not confirm the interpolation description, and Frontier now links here.

07 · Difficulties

What fought back

A New 3DS is an 804 MHz ARM11 with 124 MB for the app, a GPU that speaks neither OpenGL nor anything a PC port has met, and a debugger that is a text file on an SD card. Every layer of the stack assumed a PC. These are the fights worth retelling: each one's root cause and what it took.

The shape of the problem

G-Diffuser is the F-Zero X decompilation running on libultraship, a runtime that replays the N64 display lists on a modern GPU through a software Fast3D interpreter. On a PC that interpreter is a rounding error. On the 3DS it was the entire frame budget: a 16.7 ms frame with a CPU that spent 35 to 58 ms just building the frame in the first emulator measurements. The GPU was idle the whole time. The port therefore turned into a CPU project disguised as a graphics project, which is the opposite of what a console port normally is.

ConstraintWhy it hurt
ILP32 pointersEvery "small address means N64 offset" heuristic in libultraship and the bridge misfired: the image sits at 0x100000 and the heap at 0x8000000, both inside the N64's 8 MB RDRAM range.
PICA200 fixed-function GPUNo fragment shaders. Every combiner mode had to fit three TexEnv stages, and fog goes through a depth LUT that F-Zero X's depth range defeats.
Debugging on hardwareMostly a log file on the SD card and Luma exception dumps. The Old 3DS test unit has no GDB stub, so profiling there meant performance counters.
Azahar is not a 3DSSlower than hardware on some screens, faster on others, no instruction cache, no memory permissions, stale screenshots. Hardware was the only truth, and hardware needed the user's hands.

The boot fights (2026-08-12 → 08-14)

Getting from a linked binary to a race took three days and six distinct root causes, each of which presented as the same symptom: a black screen.

08-13
Negative-size bzero

The non-Expansion-Kit path computed a negative buffer size and cleared memory until it hit an unmapped page. First black-screen hang.

08-13
Phantom 64DD

A NULL dereference in disk-drive detection code that the PC build never reached.

08-13
std::filesystem::absolute vs sdmc:

The archive loader normalised sdmc:/… into a nonsense path, so the asset index came back empty and every texture was missing. Fixed by treating device prefixes as roots.

08-13
Double buffer swap

libultraship and citro3d both presented; frames alternated between real and stale.

08-13
The SETTIMG low-address guard

libultraship drops texture pointers below a threshold on the assumption that small numbers are N64 offsets. On the 3DS the heap starts at 0x8000000, under that threshold, so every texture was dropped. This is the bug class that recurred for two weeks.

08-13
DMA resolver misrouting

Program globals were being redirected to the RDRAM shadow, freezing the race the moment it loaded.

08-14 01:01
First race

"omg it works ! i can race ! it looks messed up and runs at 12 to 20fps but it works!"

The visual fights (08-14 → 08-27)

Pink road: fog through the wrong door

citro3d routes fog through the PICA fog unit, a 128-entry lookup table indexed by fragment depth. F-Zero X compresses the whole track into a clip-depth band about 0.01 wide (0.98 to 0.995), which lands on two LUT entries, so every road fragment got full fog. A first "batch split" fix was proven irrelevant by a probe line that produced an identical frame. The real fix bypasses the LUT: the interpreter's per-vertex fog factor is written into the primary-colour alpha and blended in one TexEnv stage. Solved 08-20 by an Opus session while the weekly usage allowance for the project's regular model (the "Fable" tier) was out.

Scrambled HUD: eight wrong fixes, one right one

The speedometer, lap and time readouts drew as static sheared garbage. Eight decode-layer fixes (RGBA16 word swaps, tile extents, atlas strips) failed because the bug was upstream: the bridge's file-path backstop required non-zero high 32 bits of a pointer, always zero on ILP32, so it silently dropped 13 SETTIMG commands per frame. The counter that said skip=13 had been in the log for days. User on 08-20: "omg finally!!". Lesson recorded: evidence-first tripwires at consumption points beat plausible-layer guessing.

Yellow Falcon: the decal that tiled seven times

The Blue Falcon wore yellow scribbles. Not a decode bug: libultraship strips clamp flags for mirror-and-clamp tiles and the backend sampled unclamped, so the Falcon's own stripe decal repeated across the body. Then a second cause: the body tint (gDPSetEnvColor) did not flush pending triangles, so six machines sharing a batch were all painted in the first machine's colour. Right in the select screen (one machine per batch), wrong in the race.

Black sky wedge

A black polygon swallowed the sky depending on camera pitch. Three theories died (fog regression, 4:3 skybox under-covering the 16:10 display, cloud layer). Fourth was right: a 64×1 sky gradient padded to 64×8 for power-of-two, clamped at the padded edge, sampled zeroed padding when the game's wild T-coordinates ran off the tile window. Proven by flooding the padding magenta. Third bug of the power-of-two padding family.

Audio: the fight nobody mentioned

The first hardware run on 08-21 came with a confession: sound had never worked, in the emulator or on the console, and the user had not said so because the DSP firmware setup looked correct. The memory file had recorded "HLE audio works" because producer counters ticked. The output was silent. What followed was the roughest afternoon of the project.

User, 08-21 18:08 → 20:40noooo the sound didnt work on the machine :( … SD is back in, round trip. no sound again … finally heard the tone ! … SD back in.. no sound :( … uh dude no, i heard the tone sound before even. and i heard the music fine earlier. its not azahar.

Two mistakes compounded. An agent was killed mid-report and its audible test run was attributed to the wrong build, which started a three-hour phantom hunt. The true cause was that the ROM-side audio driver itself needed host porting (about 700 lines across the load, sequence player, thread and heap files) plus linear interpolation for real DSP output, because the polyphase path was silent on hardware and fine in the emulator. At 21:22: "working! giant win." The process lessons went straight into the memory file: never kill an agent on partial narration, always bind an audible run to a verified binary, silence verdicts need a long soak.

HOME, twice

On 09-01 the user found that pressing HOME crashed the game instantly. The audio threads were hitting the DSP through the ndsp suspend. Fixing that exposed a second bug: closing from the HOME menu hung the console. libctru wakes the app with APTCMD_WAKEUP_CANCEL, no restore; the drain thread woke on a shared sticky park event, re-waited, and LightEvent_Wait on an already-signalled sticky event returns instantly, so a hot spinner on the producer's core starved the producer and the join never returned. Fix: per-thread park events, stop-before-signal, flushed exit receipts. Verified on hardware 09-02: HOME in, HOME out, close from HOME, lid sleep, power off.

Seven weeks later, on the Old 3DS, closing from HOME hard-locked the console (10-26). At exit the 64DD save flush asked for one contiguous 73 KB buffer; about 390 KB was free, but not in one piece. bad_alloc went to std::terminate, then svcBreak, which locks the console instead of exiting. The flush now streams from its journal with no heap allocation, and a break during teardown exits.

Concurrency: the round-4 hard crash

The render thread on core 2 delivered the best numbers of the project and, after a full Grand Prix, a data abort in the texture cache LRU splice while the main thread was mutating the same list during podium and venue loads. The fix made the render thread the sole owner of the cache: clears become requests drained on the render thread, deletes are queued, and a receipt (texcacheMainMut=0) proves no main-thread mutation happened.

Bugs only the Old 3DS could show (09-24 → 10-26)

Each of these looked fine in the emulator. The per-card numbers are on The road to 60, all over again.

FightSymptomRoot cause and fix
HOME freezeHOME froze the game image; power-off neededNot audio, not the file system, not core 1: probe apps ruled each out. The bottom-backlight display session was held open for the process lifetime and the HOME menu blocked on it. Open, call, close per toggle; 7/7 resumes clean
Cold boot crashThe first release candidate crashed after graphics setup, no dumpA four-build bisect: size-optimised code crashes cold, stock code boots. Code generation reverted to stock; the underlying bug was never found
Verify-mode freezeThe first native-renderer build froze at race startA state save/restore copied the GPU's live command queue too, rolling back a completion the hardware had already acted on. The emulator's equivalent is synchronous and could not show it
Saves never persistedRecords lost after the first saveThe SD rename fallback triggers on one error code; the SD archive answers a different one. A crash-safe replace with boot recovery fixed it, on both models
"all 3 crashed"Random pixels at boot, no logFramebuffers moved to VRAM, which an installed title maps read-only for the CPU; the first bottom-screen write aborted. The emulator does not enforce that permission
Fragmentation, not exhaustionAfter six course loads the fast track path quietly fell back to the slow oneA 96,768-byte allocation failed with 1.6 MB free. The track now lives in one arena reserved at boot

Process failures, honestly

  • Agents died on token limits and API overloads, sometimes with zero commits. The rule became "commit per milestone, write the progress file first". On 09-30 all seven running agents hit the limit together.
  • Agents tested levers singly. Four LOCKED-60 levers each passed alone and together broke the HUD. A combined-lever screenshot became mandatory.
  • Agents fought over the emulator, killing each other's sessions and deleting SD artifacts, until a lock file. A cleanup command with an empty run id later deleted the shared emulator directory; scripts now refuse an empty id.
  • The screenshot oracle lied. Captures were a frame or two stale. The user's eyes became the only trusted display oracle.
  • Gate holes. Parity gates ran with stereo on, so two categories refused and were never exercised; found and re-run.
  • Bad comparisons. A 617 KB "heap regression" compared a 2D multiplayer log with a 1P 3D log; an early per-category baseline was measured on warm-up frames.
  • Pauses. The weekly allowance ran out three times in the first phase and stopped the fleet twice in the second; each pause left a RESUME document.

08 · Frontier

Why this is bold, and where it is new

The user asked on 08-23 how to explain to an unimpressed friend why this port matters. The short version: when it started, no libultraship game was known to run on a handheld this weak, and no N64 port on 3DS was known to hold a native 60 Hz without interpolating, let alone in stereoscopic 3D; this one did both in three and a half weeks, with a fleet of AI agents doing the typing. A second phase then replaced the interpreter for race frames and took the game to the Old 3DS, a console with a third of the clock, and a third added online multiplayer and one-submission stereo. Others have moved since; the items below say where each claim stands now.

The field before this project

Two deep-research passes (08-14, 09-01) and a targeted literature check (09-02) mapped the state of the art. The findings framed every architectural decision:

PrecedentWhat it didWhat it means here
sm64_3ds (the well-known Mario 64 3DS port)Native decomp with a hand-written 3DS renderer, plain switch dispatch, audio on a second core. Its README describes a naive frame skip below 30 fps and lists a separate 60 FPS patch.The proven fallback in this field is some form of 30 Hz, interpolated or not. This project refused that and targeted native 60.
libultraship ports (Ship of Harkinian, 2Ship, Starship, and this game's own PC port)Desktop-only. x86-64 and ARM64 with OpenGL, Metal or DirectX. Never compiled for devkitARM.The LUS carve-out spike (08-12) proved LUS builds on devkitARM with a 13-line newlib patch, the first such build the research knew of (two other libultraship 3DS projects had appeared by 10-08; item 1).
Every N64 port ever shippedWalks the display list on the CPU every frame and re-emits it to the GPU.The 09-01 research verdict: nobody has shipped beyond the per-frame walk. GPU vertex transform was the field's number-one untried idea, listed as a TODO by the sm64 port's author.
Cross-core render pipelining on 3DSNo precedent found in any homebrew title. Core 2 needs a Luma kernel grant; core 3 is the OS's.The render thread (09-02 → 09-03) was, as far as that research could find, the first game-logic-ahead render pipeline on the console. The 10-08 field search later found Smash 64 for New 3DS rendering on another core too (item 5).
An earlier draft of this table described the sm64_3ds port as 30 Hz logic interpolated to 60; that was not accurate. See Pushing the boundaries for the correction.

Nine things this project did first, or did differently

Each item carries its journey: where it started, what changed, and where it stands in the v1.1.0 work. A first label is used only where a research pass looked for precedent and found none, and says when; everything else is labelled as a difference.

1. libultraship on a 3DS

first, as far as our research found

The whole runtime, its Fast3D interpreter, resource manager and archive loader, compiled for the ARM11 under devkitARM. When this started, the research passes (08-14, 09-01) found libultraship only on desktops. By the 10-08 field search two other libultraship-based 3DS projects had appeared (a SpaghettiKart-based Mario Kart 64 and a Ship of Harkinian port); their start dates are not in our record, so the claim is dated: no libultraship 3DS build was known on 08-12.

08-12A carve-out spike builds libultraship under devkitARM with a 13-line newlib patch.
First phaseKept as pure-delta patches over two untouched submodules (48 files).
Now82 decomp patches; the 41 libultraship patches are folded into a vendored carve that is the source of truth, and its interpreter runs only under preset = off.

2. Native 60 on a software display-list interpreter

did differently

The first phase's position on the New 3DS, full Grand Prix, trace off: median 59.6 fps, tenth percentile 51.4, 53% of frames at the cap, without dropping the logic rate or interpolating. When it landed, the 09-01 research had found no N64 port on 3DS holding 60 this way. The 10-08 field search found others reaching 60 by interpolating over 30 Hz logic, and Smash 64 for New 3DS aiming at 60 without promising it, so this is listed as a difference, not a first.

08-1412–20 fps in the emulator; 08-21, 48–50 on hardware.
09-03With the render thread on core 2, the last hardware rounds reach a 59.6 median.
NowHistory: the shipped default is the native renderer below. This path stays reachable with preset = off.

3. Exact stereoscopy, then both eyes in one GPU submission

did differently

Stereo is an asymmetric-frustum shift folded exactly into the projection, x′ = x ± k·(z − dc·w), so depth, fog and the combiner are bit-identical per eye and the 2D race markers can sit at their ship's depth. The second eye has been produced three ways. Other 3DS N64 ports also replay per eye (rt64-3ds, the SM64 lineage); what is different here is how little the right eye costs, and that the last step came from a suggestion by a 3DS homebrew developer.

08-27Armed on hardware: each draw re-issued for the right eye from the same packed vertices. "3D works great."
Native forkFrame replay: the body is emitted once; the right eye resubmits the same command lists with one uniform changed.
v1.1.0One submission: the GPU runs the body, jumps to a right-eye prologue, runs it again. Old 3DS 3D menu 40.2 → 44.6 fps. Still not a locked 60 in 3D on either console (item 7).

4. GPU vertex transform: shelved, then shipped

did differently

The 08-28 moonshot moved the unlit vertex transform onto the PICA vertex shader: 220,000 draws with zero fallbacks, 62% less vertex time in the emulator, imperceptible on hardware, and broken on the venue floor because 24-bit floats cannot hold ±32000-unit coordinates. It was shelved on that evidence. The native fork brought it back for the meshes it retains, with the precision problem solved: the shader adds an integer eye offset first, which float24 represents exactly.

08-28Built, measured, shelved: too small a slice, and a precision bug.
Native forkRevived as gpuxf for retained track and machine meshes, with lighting and texgen.
NowOn by default on both consoles.

5. A render thread inside the game's own frame protocol

did differently

Rather than a mailbox, the fork happens where the N64 would have started the RSP task (osSpTaskStartGo) and the join is the game's own DP-done wait: a true one-frame-ahead pipeline with two-deep backpressure. The 09-02 research found no precedent; the 10-08 field search found Smash 64 for New 3DS also rendering on another core, so this is a different design, not the only multi-core renderer.

09-02Brief and first build; core 2 reached through a Luma kernel grant.
09-03Hardware round 4: median 59.6 fps.
NowOff at release on both consoles: the native renderer needs a sequential frame, and core 2 runs audio instead.

6. Fixed-function combiner totality

did differently

Every N64 colour-combiner mode the game uses maps to three or fewer PICA TexEnv stages; fog is a per-vertex factor blended in a TexEnv stage because the PICA's depth LUT cannot resolve the game's depth range. Zero unsupported modes.

First phaseA census of every mode; worst case three stages.
FixesDecal clamp, environment-colour flush and CCMUX-11 made the translation total.
NowOur own walker reuses the same combiner keys verbatim, and the native race renderer blends fog in the same kind of stage.

7. Skipping the display-list walk, on the hardest target

first, as far as our research found

For what a race spends its frame on, the native renderer writes the GPU's own command words from recorded and baked data instead of walking the display list (see The road to 60, all over again). It shipped first on, and matters most for, the Old 3DS: a third of the clock, no L2 cache, no second application core. No other N64 decompilation, recompilation or emulator project the 10-08 search found claims a sustained 60 on an Old 3DS, and that was still the case at release.

09-07The fork begins; the Old 3DS, ruled out on 08-11, is revisited from 09-24.
10-08An internal card holds 59–60 fps in Grand Prix races on Old 3DS hardware.
NowShipped binary, Old 3DS, full 30-racer Grand Prix in 2D: 57.3 fps mean (log stalls excluded). Since v1.1.0, menus too. 3D: 59.7–59.8 in a short one-submission sample, but some courses step down to 30; the CPU is the limit.

8. Online lockstep multiplayer across Old and New 3DS

did differently

Two to four consoles race over the internet, the same Wi-Fi or local wireless, sending only inputs: about 130 bytes per tick per peer, every console simulating the same race in deterministic lockstep. A small rendezvous server (one standard-library Python file, UDP 27200) exchanges endpoints from a five-character room code, punches through NATs and relays when it cannot; agents deployed and operated it on AWS. Old and New 3DS can share a room. No precedent search was done, so this is not claimed as a first.

v1.0.0Split-screen 2–4 players on one console.
10-21Lockstep spike, then online, local-wireless and full-screen-per-console play merged the same day.
v1.1.0Frame-advantage pacing, drop-outs handed to a CPU driver, desync reports. Old 3DS: 2D aims for 60, 3D locked to 30.

9. The 64DD Expansion Kit on a handheld

did differently

The course editor, machine creator and 64DD content run as their own New 3DS build, streaming the player's own retail Japanese disk image from the SD card instead of holding 62 MB in RAM. We did not search for earlier 64DD efforts on a handheld, so no first is claimed.

Native forkExpansion Kit builds fit both consoles' memory; the editor's black screen fixed.
v1.0.0Released for New 3DS only: on Old 3DS the editors crawl and disk music breaks up.
NowMostly emulator-verified; a 10-26 New 3DS hardware card ran an Expansion Kit build and held 60 in a short 3D race log.

Why bold

The conservative plan was available from day one and was declined at every fork:

  • Native 60 Hz instead of 30 Hz + interpolation. The deep-research report on 08-21 said the proven path was the sm64 way. The user's instruction on 08-14: "only list options that work towards this running acceptably in 3D at 60fps."
  • Stereo before the frame budget was safe. The stereo foundation was merged flag-off on 08-20 and armed on 08-27 while crowd frames still dipped to 30 fps, on the argument that it costs CPU submit time, not GPU time, and had to be designed in.
  • Multi-core on a console that hides its cores. Core 2 is only reachable with a kernel capability that Luma grants to homebrew; the choice to depend on it was made after the 09-02 research and a hardware confirmation that the grant holds for a .3dsx and a .cia.
  • Patch stack, not a fork. Keeping upstream pristine cost real pain (every patch a pure delta, regenerated by three-way merge) and kept the door open to upstreaming. One leak fix in libultraship was identified as an upstream bug on 08-28. The decomp side still works this way; libultraship was later frozen into a vendored carve (item 1).
  • Shelving on evidence, not on effort. GPU transform, the bridge output cache, the DSP batch-break fold, the double-height stereo target and the machine texture atlas were each built to the point of measurement and then declined. The project's default-off branches are its most expensive documents, and one of them came back: GPU transform now ships on by default, once the native renderer gave it meshes worth transforming (item 4).

Innovation in method

The technical results rest on a way of working that is itself new. Every performance change shipped with a killswitch and a receipt line, so hardware could A/B it without a rebuild. Every hypothesis got a probe before a fix (the magenta padding flood, the fog probe line, the CRC tripwires that pinned the HUD bug). Every hardware round had a test plan written for a human and a log format written for a machine. The agentic workflow page covers how that discipline was enforced across a fleet of agents that could not see the console.

09 · Agentic workflow

How the fleet was run, and how the ship was steered

One human, one orchestrating Claude session that lived for the whole project. In the first phase (2026-08-11 to 09-03), 147 subagents worked in 96 git worktrees. A second phase (09-07 to 10-19) ran the same discipline at a larger scale, for longer, with a wider toolset — covered in its own section below. Throughout, the human never opened an editor, and held the only console, the only SD card, and the only pair of eyes that could be trusted.

223
messages typed by the user in the first phase (08-11 to 09-03)
147
background subagent transcripts, first phase (the Agent tool only; the 1,103 workflow agents are counted separately, see Cost)
96
separate git worktrees, each on its own branch, first phase
98
feat/3ds-* branches, first phase
10
deep-research workflow runs, first phase
15
active days between 08-11 and 09-03

The topology

One long-lived session was the orchestrator. It held the project memory, wrote the briefs, merged the branches, and talked to the user. Work was fanned out to background agents, each given a worktree, a branch named for its lever, a brief file committed into docs/research/, and a progress file it had to write before it was allowed to finish. Agents never talked to the user; the orchestrator relayed. Agents never touched mainline; the orchestrator merged after an emulator smoke test and, for anything risky, after a hardware verdict.

The humanSets goals, plays the builds,judges what he sees, says yesor no to every merge. Neveropens an editor.Orchestrator sessionOne long-lived Claude Code session:project memory, briefs, merges, the onlyvoice the human hears. RESUME notes atevery credit pause.Review toolsAdversarial review passesafter merge windows (findingsranked, confirmed orsuspected); frame, hash andscreenshot diff tools; a localreview page for thepost-mortem's own notes.goals,verdicts,photosauditthemergesHardware laneOne combined test build perconsole (auto A/B, auto logs).In: SD card trip, or 3dslinkpush to the netloader (TCP17491), or in-game reload (TCP17492). Out: live log to aserver on the Mac, log.txt onSD.THE FLEETSubagentsOne worktreeand one branchper lever, acommitted briefand a progressfile.Killswitch +receipt linefor everychange. 147 inthe first phasealone.WorkflowsScriptedfan-outs: deepresearch(search agents→ fetch →verify →synthesise),parallelcensuses andreviews.Emulator farmAzahar (one Qt instance, onelock) plus RetroArch Azaharinstances, each with a privateSD, behind a three-slotmachine-wide pool. Tick-exactinput replays with per-framestate hashes.briefs ↓ commits,progress ↑merge, build← logssmoke,parity,shotsNew 3DS + Old 3DSThe only ground truth forframe rate, feel and visualbugs. Old 3DS mostly by SDcard.Rendezvous server (AWS)v1.1.0 online play: UDP 27200room codes, hole punching,relay fallback. EC2 t4g.nano,run by agents throughTerraform and an ops script.build in ↓ log out ↑online race: endpoints, punch, relay fallback (UDP 27200)consoles then race in lockstep, ~130 bytes per tick per peercarries the SD card,presses Y on the netloader
The humanSets goals, plays the builds, judgeswhat he sees, says yes or no to everymerge. Never opens an editor.Orchestrator sessionOne long-lived Claude Code session:project memory, briefs, merges, theonly voice the human hears. RESUMEnotes at every credit pause.goals and verdicts ↓ reports ↑SubagentsOne worktree and one branch perlever, a committed brief and aprogress file. Killswitch + receiptline for every change. 147 in thefirst phase alone.briefs ↓ commits ↑Emulator farmAzahar (one Qt instance, onelock) plus RetroArch Azaharinstances, each with a privateSD, behind a three-slotmachine-wide pool. Tick-exactinput replays with per-framestate hashes.smoke, replay parity, shotsWorkflowsScripted fan-outs: deep research(search agents → fetch → verify →synthesise), parallel censuses andreviews.fan-out scriptsReview toolsAdversarial review passes aftermerge windows (findings ranked,confirmed or suspected); frame, hashand screenshot diff tools; a localreview page for the post-mortem'sown notes.audit after mergesHardware laneOne combined test build per console(auto A/B, auto logs). In: SD cardtrip, or 3dslink push to thenetloader (TCP 17491), or in-gamereload (TCP 17492). Out: live log toa server on the Mac, log.txt on SD.merge → one build per consoleNew 3DS + Old 3DSThe only ground truth for framerate, feel and visual bugs. Old3DS mostly by SD card.SD / netloader in, log outRendezvous server (AWS)v1.1.0 online play: UDP 27200room codes, hole punching,relay fallback. EC2 t4g.nano,run by agents through Terraformand an ops script.online races only
How it actually ran. One human, one orchestrator, a fleet that never talked to the human. The hardware lane changed over time: SD card only through the first phase and the start of the second, then netloader push, in-game reload and a live log server, and from 10-21 one combined test build per console instead of one card per feature. The emulator farm went from one locked Azahar to a slot pool of parallel instances. The rendezvous server (dashed) exists only for v1.1.0 online play.

The rules the fleet lived by

These accreted from failures (see Difficulties) and were copied into every brief as a "common rules" block:

RuleBorn from
Patch as pure delta. Upstream submodules stay pristine; every change is a patch generated against the full current stack; the README lists the order; a clean-stack round-trip must pass.Double-carried hunks in the rival-detail patch, twice.
Killswitch for everything. Every lever has an ini key defaulting on, and later a row in the bottom-screen DBG tab, so hardware can A/B without a rebuild.The first hardware rounds could only compare builds.
Receipt for everything. A lever must print a log line proving it engaged ([rt] mode=ahead, [c3d] anchor=…, texcacheMainMut=0).A silent audio agent whose run was attributed to the wrong binary.
Commit early, progress file first. Agents die on token limits and API overloads; a dead agent with zero commits costs a day.Traffic, sky3b, P2 and the first bridge-cache agent.
One emulator, one lock. /tmp/azahar.lock, touched every 30 s while held; wait in turn; never kill another run.Agents killing each other's Azahar and deleting SD artifacts.
Combined-lever screenshot before staging. A race frame with every lever on, captured through the window id, inspected by eye.The HUD-destroying interaction between trifast and the atlas.
Merge ini keys, never overwrite. The user's SD ini holds their 3D and display choices.A round that shipped with stereo silently off.
Never re-apply the patch list onto a patched tree. Reset both submodules first.A corrupted decomp tree in the final week.

Steering from outside the code

The user's messages are short, frequent, and almost entirely about direction, evidence and verdicts. Reading all 223 of them in order, the pattern is a captain, not a programmer: set the goal, demand parallelism, bring the physical evidence, say yes or no, and protect the budget.

Setting the goal and refusing the safe path

08-11 03:44lets create a plan for team of agents to work. can we segment plan so we can have multiple work trees and multiple contributing agents ?
08-14 05:05only list options that work towards this running acceptably in 3D at 60fps
08-21 22:49played a bunch, running well. no hitches. music still plays. rest of issues are what's left. would love to see the framerate at a crispy 60. then... maybe 3D?
08-28 04:27moonshot! lets go for it

Demanding parallelism

08-14 01:10i didnt see any hard crashes right now, so lets see about the visual fixes. any work that can be done in parallel ?
09-02 21:59lets do /deep-research on whats left, come up with plan, and execute subagents for all parallel work

Being the oracle

The agents could not see the console. The user ran the SD-card loop dozens of times ("SD in", "SD returned", "SD card is back in" appear 36 times) and reported what a human sees: shapes, colours, hitches, feel. Those reports were the only ground truth for the visual bugs and for every fps number on the Road to 60 page.

08-14 13:21yep definitely looks like the game but its still quite buggy. some of the elements jitter/shimmer, textures change. opening tunnel doesnt have a roof, might have to do with backface culling?
08-21 15:54wow it runs amazing ! like you were suggesting, maybe even better than the emu. sound doesn't work btw. sound has never worked on the emu either.
08-28 04:22minimal appears to be a 8 to 9 fps gain ! sometimes a little more. def worth it.
09-03 16:32next thing to consider: 3D works great for 3D elements, but UI elements are a little less consistent. one thing thats really jarring is the 1P/2P/3P markers during a race. they sit on the most foreground in 3D but the ships are somewhere in the distance

Pushing back on the machine

08-21 00:23nothing new to report here. all the same issues. anything we can do ? instanity is doing the same things and expecting different results. /deep-research different methods of fixing these issues.
08-21 20:40uh dude no, i heard the tone sound before even. and i heard the music fine earlier. its not azahar.
08-15 16:33also mark that it was done with opus and not fable so it should be reconsidered if this is the best approach

Guarding the budget and the record

08-14 14:06we ran out of fable credits until next week, lets capture all the inflight tasks and store state so we can pick up the work later.
08-28 13:17lets try pushing the last lever. lets also write up the README for release. lets make sure we clearly declare this was just for educational purposes, not affiliated with nintendo, doesnt provide ROM or assets
09-03 13:20also, is there any way you could create a webpage with some histograms of the performance + documentation on how the changes all affected performance?

Working through the night

Twice (08-20 and 08-21) the user went to sleep with the instruction to keep working. Each night session ran a fleet against a frozen mainline, wrote a morning-status document, and left a candidate branch to promote after the user's eyeball. The 08-20 candidate was promoted with "sure, promote it. i dont see any large regressions. probably better"; the 08-21 night produced the S7 interpreter memo and the async frame-mirror copy that took the menus from 15 to 20 fps in the emulator.

What the orchestrator did that an agent could not

  • Held the memory. A single project memory file (591 lines by the end) was rewritten at every milestone with state, gotchas, and the next step, and three RESUME documents were committed at every credit pause. Every context compaction and every new session restarted from it.
  • Wrote the briefs. Each lever got a brief with the hypothesis, the file locations, the measurement protocol, the killswitch name, the receipt format and the report format. The briefs are in the repository.
  • Finished what agents dropped. The texture-cache crash fix, the viewport fix and the stereo anchor were completed by the orchestrator after the assigned agents died on API overloads.
  • Ran the hardware loop. Build, package (.3dsx + .cia + test plan), copy to SD, eject, read the log back, compute the fps distribution, decide merge or revert.

The second phase: longer, wider, with an adversary

The first phase was a 24-day sprint to a single target. The second (09-07 to 10-19, 43 calendar days) built a different renderer, a second console tier and a second game mode on top of a build that already worked, which changed the shape of the work more than its length: many more parallel workstreams, each still in its own worktree with its own brief and progress file, left behind 199 internal progress notes by the end, far more than the first phase's smaller written record. The rules in the table above (patch as pure delta, killswitch for everything, receipt for everything, commit early) held and were leaned on harder, because more of the work now ran unattended for longer between human checks.

Three things were added to the loop that the first phase did not have:

  • A network link to the console. The first phase's only path to hardware was ejecting the SD card, copying files, and reinserting it. The second phase added a lightweight network link (a homebrew-loader push plus a small device-link tool) so a build could reach a development console directly over Wi-Fi, cutting the time between a build finishing and a human seeing it run.
  • An emulator replay lane for proof without hardware. A record-and-replay harness captured exact input sequences and per-frame state hashes, so a new render path could be checked frame-for-frame against the old one — bit-identical pixels, identical game-state hashes — without needing the console or a human at all. This is what let dozens of rendering changes be verified as behaviourally identical before ever being staged for a hardware round, the same role the emulator played in the first phase but run far more often and more mechanically.
  • A standing adversarial review. After a window where many parallel branches landed in quick succession, a dedicated review pass audited the merged result for what the individual branches' own testing could not catch — not new bugs in any one branch, but damage from combining them: two agents measuring the same thing with code that quietly disagreed, a comparison tool whose flag got pasted out of existence, a safety wait that had become unreachable. Findings were ranked by severity and marked confirmed or suspected, and the practice repeated after later merge windows.

What stayed the same: one orchestrating session, one human approving every merge into the branch that builds toward release, and no agent ever touching the only SD card or the only console directly.

Where the workflow was pioneering

Multi-agent coding is usually demonstrated on web apps with a test suite. This project ran it against a target the agents could not observe, with a human-in-the-loop measurement cycle measured in SD-card round trips, on a codebase made of three repositories (decomp, runtime, port) with a patch stack instead of a fork. The Frontier page names the devices that made that work transferable. What this page adds: the second phase runs the same argument at a larger scale — the same devices and discipline, held across more than twice the calendar time, more console configurations, and a ground-up renderer rewrite.

10 · Cost

What it cost to build

A human team (estimate)
2–8 engineer-years, $0.5M–$2.3M
About 6–23 months with a team of 4 senior engineers. Method and inputs are under "The human estimate" below.
What it took here
$16k–$20k API · ~125–150 h
Claude at published API list prices, plus one person directing, testing on hardware and copying SD cards, over 11 calendar weeks (2026-08-11 → 10-26). The hours are the author's own figure, checked against the transcripts; see "The author's time".

Priced the same way, the human route costs roughly 11–69× more, even after billing the author's hours at a senior engineer's rate ($140/h, which puts this project at $33k–$41k). The token and API figures are counted exactly from the session transcripts; the human figures are estimates and the author's hours come from the transcripts and the author, and the page says how each was made.

Updated 2026-10-26. The first version of this page priced only the first phase (08-11 to 09-03): 3.17 B tokens and $4,974. This version counts every phase through 2026-10-26 (census run 2026-10-26 23:40 UTC; the last day is still open): 22.59 B tokens and $19,559 by the same method, or $15,567 billing each cache write at the duration it actually logged (see "Prices used").
22.59 B
tokens processed, all sessions, subagents and workflow agents, 08-11 to 10-26
$19,559
at API list prices, every cache write at the 1-hour rate (the first-phase page's method)
$15,567
at API list prices, each cache write at the duration it logged (5 minutes for most)
107,419
API calls (assistant turns) after de-duplication
22.0 M
output tokens: the text and code the models actually wrote
777
human prompts typed into the project's sessions
588
background subagents launched with the Agent tool
1,288
short-lived agents inside deep-research workflows

By phase

PhaseDatesAPI callsTokensOutputSubagents / workflow agentsCost, 1 h writesCost, as logged
New 3DS, to v0.9.008-11 → 09-0322,5523.20 B13.3 M147 / 1,103$4,998$4,419
Native fork, to v1.0.009-07 → 10-1962,05115.21 B6.8 M317 / 185$12,427$9,463
v1.1.0 work10-20 → 10-2622,6244.14 B1.7 M124 / 0$2,074$1,625
Between phases09-04 → 09-0619241.7 M170 K0 / 0$60$60
Total08-11 → 10-26107,41922.59 B22.0 M588 / 1,288$19,559$15,567
Opus 5.5Fable 5Opus 5Fable 5.1Sonnet 5Opus 4.8Haiku 4.5
0.03.57.010.413.9New 3DS|to v0.9.0 · Fable 5: 3.6New 3DS|to v0.9.0 · Opus 5: 0.0New 3DS|to v0.9.0 · Fable 5.1: 1.0New 3DS|to v0.9.0 · Opus 4.8: 0.4New 3DS|to v0.9.0 · Haiku 4.5: 0.05.0New 3DSto v0.9.0Native fork|to v1.0.0 · Opus 5.5: 7.2Native fork|to v1.0.0 · Opus 5: 3.2Native fork|to v1.0.0 · Fable 5.1: 1.0Native fork|to v1.0.0 · Sonnet 5: 1.1Native fork|to v1.0.0 · Haiku 4.5: 0.012.4Native forkto v1.0.0v1.1.0 work · Opus 5.5: 2.1v1.1.0 work · Sonnet 5: 0.02.1v1.1.0 workthousands of USD, 1-hour cache writes
The second phase cost about two and a half times the first. It ran 43 calendar days against 24, on more agents in parallel, with larger contexts. Opus 5.5 carried more than half of the second phase's spend and nearly all of the work since v1.0.0; Fable 5 carried most of the first phase.

Prices used

Published Claude API list prices in dollars per million tokens: the same table the first version of this page used, with the two models that arrived since (Opus 5.5 and Sonnet 5) added at their published rates. Cache reads are 0.1× input except where listed.

ModelInputOutputCache write (1 h)Cache write (5 min)Cache read
Claude Fable 5.1$10$50$20$12.50$0.25
Claude Fable 5$10$50$20$12.50$1.00
Claude Opus 5 / Opus 4.8$5$25$10$6.25$0.50
Claude Opus 5.5$4$20$8$5.00$0.20
Claude Sonnet 5$2$10$4$2.50$0.20
Claude Haiku 4.5$1$5$2$1.25$0.10
A correction to the first version. It billed every cache write at the 1-hour rate, on the belief that 1 hour was the TTL these sessions used. The usage records say otherwise: of 1.25 B cache-write tokens that carry a duration, 95% were 5-minute writes. Billed as logged, the first phase comes to $4,419, not $4,998, and the whole project to $15,567. Both figures are shown because the first is the like-for-like update of the number this page used to print and the second is the better estimate. The user's Claude plan is not billed per token; these are what the same usage would have cost on the API. Web searches and context compactions are not in the transcripts and are not counted (in the first phase they came to about $13).

By model

ModelAPI callsOutput tokensCache-read tokensCost, 1 h writesCost, as logged
claude-opus-5-545,6743,257,06510,477,297,093$9,299$6,711
claude-fable-515,1909,293,4002,151,927,954$3,647$3,339
claude-opus-516,9841,996,9613,658,547,740$3,165$2,728
claude-fable-5-14,7153,488,5851,042,680,573$1,989$1,485
claude-sonnet-520,3691,815,6613,583,164,321$1,072$946
claude-opus-4-84,1322,038,747398,856,214$385$356
claude-haiku-4-5-2025100114876,3406,114,482$2$2

Spend by day

$0$615$1,230$1,845$2,4612026-08-11: $9408-112026-08-12: $62026-08-13: $39008-132026-08-14: $1,091first race2026-08-15: $4108-152026-08-16: $02026-08-17: $008-172026-08-18: $02026-08-19: $008-192026-08-20: $2642026-08-21: $98908-212026-08-22: $902026-08-23: $008-232026-08-24: $02026-08-25: $008-252026-08-26: $02026-08-27: $76608-272026-08-28: $3002026-08-29: $008-292026-08-30: $02026-08-31: $008-312026-09-01: $1592026-09-02: $49609-022026-09-03: $3132026-09-04: $4509-042026-09-05: $152026-09-06: $009-062026-09-07: $2012026-09-08: $009-082026-09-09: $02026-09-10: $009-102026-09-11: $02026-09-12: $009-122026-09-13: $7582026-09-14: $009-142026-09-15: $02026-09-16: $009-162026-09-17: $3082026-09-18: $809-182026-09-19: $92026-09-20: $12209-202026-09-21: $02026-09-22: $009-222026-09-23: $1,4232026-09-24: $41809-242026-09-25: $3232026-09-26: $38009-262026-09-27: $1672026-09-28: $009-282026-09-29: $687Old 3DS boots2026-09-30: $1,14309-302026-10-01: $1,3762026-10-02: $84010-022026-10-03: $2962026-10-04: $010-042026-10-05: $02026-10-06: $010-062026-10-07: $6652026-10-08: $2,14010-082026-10-09: $02026-10-10: $010-102026-10-11: $02026-10-12: $010-122026-10-13: $02026-10-14: $010-142026-10-15: $4212026-10-16: $32510-162026-10-17: $882026-10-18: $3610-182026-10-19: $292v1.0.02026-10-20: $1610-202026-10-21: $7582026-10-22: $31210-222026-10-23: $2332026-10-24: $12610-242026-10-25: $3442026-10-26: $28610-26USD per day, 1-hour cache writes
48 days with API calls in 77 calendar days. The first phase spent in a few large bursts between plan-allowance pauses. The second phase's heaviest days were 09-23 (fitting the Old 3DS build into its memory), 09-29 to 10-02 (the first Old 3DS hardware session through the first native-renderer race) and 10-08, the end of the 3D-at-60 push and the most expensive day of the project at $2,140. Hover a bar for its value.

The human estimate

A rough comparison, not a quote. The scope a team would face is what shipped: a citro3d rendering backend, a 32-bit sweep, audio on the DSP, stereo, lifecycle, an owned native renderer sized to the Old 3DS's instruction cache, the 64DD Expansion Kit, a bottom-screen dashboard and menus, and 2–4 player multiplayer over local wireless and the internet, on two consoles.

InputValueBasis
Code written for the port181,208 linesport/3ds at ac33c30, excluding the vendored libultraship subset, third-party libraries, generated asset headers and docs; includes the patch stack and 13,349 lines of tests, probes and harness. Build and dev tools add another 41,324, not counted below
Lines a human team would need84k–168kHalf to all of the production code. The agents wrote heavily instrumented code (receipts, A/B arms, levers); a team would write less of it for the same result
Rate100–200 lines per engineer-dayThe rate the first version of this page used for low-level port work once debugging and hardware loops are counted; 220 working days a year
Effort1.9–7.6 engineer-yearsLines ÷ rate
Cost$0.5M–$2.3M$240k–$300k loaded per senior engineer-year (US; $20–25k a month, as before)
Calendarabout 6–23 monthsA team of 4, ignoring hiring and ramp-up

A check against people who have done comparable work: Ship of Harkinian, the Ocarina of Time PC port, was built by a volunteer team from November 2021, when the decompilation was finished, to its first release in March 2022; 2 Ship 2 Harkinian, the Majora's Mask port on the same libultraship base, shipped in May 2024, six months after its decompilation was finished (Wikipedia). Those are PC ports with no memory or frame budget to speak of. The N64-on-3DS projects listed on Pushing the boundaries are hobby efforts that, by their own READMEs, mostly target the New 3DS at 30 fps or with interpolation. Nothing here contradicts the range above; nothing narrows it much either.

The author's time

The transcripts hold 777 prompts typed into this project's sessions on 49 days between 08-10 and 10-26 (US Eastern time). Grouping prompts less than 20–45 minutes apart into sittings, and adding 15 minutes to each, gives 126–157 hours at the keyboard. The author confirms the total was about 125–150 hours: hardware testing, SD-card copies, photographing screens and reading the docs leave no trace in the transcripts, but they largely overlapped with sittings or were short, so the total is close to the keyboard time. Prompts fall on every day of the week and at every hour from 8 am to 3 am, so this was not only evenings.

No code was written by hand. The author's work was direction (what to build next, what to drop), judgement on evidence (which experiments were real wins), and the hardware loop the agents could not run themselves.

Side by side

This projectHuman team, estimate
Calendar11 weeks (08-11 → 10-26), 48 days with API activity6–23 months with 4 engineers
Money$15,567–$19,559 API-equivalent$0.5M–$2.3M
Peopleone person, ~125–150 hours, no code written by hand2–8 engineer-years
What shippedv0.9.0, v1.0.0 (Old and New 3DS, Expansion Kit) and v1.1.0 (multiplayer); known gaps listed in the release notesthe same scope, plus whatever polish a funded team would add

Cost per outcome

Measure1-hour writesAs logged
Per human prompt$25$20
Per day with API activity (48)$407$324
Per line of port code (181,208)$0.11$0.09

How the census was made

docs/postmortem/gen/pa_census.py re-implements the first version's method and can be re-run. It reads every transcript under the project's two Claude Code directories (~/Code/G-Diffuser through v1.0.0, ~/Code/gdx-3ds since; 1,881 files: orchestrator sessions, Agent-tool subagents and deep-research workflow agents). Each assistant message id is one API call; the 2–5 streamed records of the same message are merged by taking the largest value of each usage field, so nothing is counted twice. Sessions started from the home directory that only mention the project, and every other project on the same account, are excluded.

CheckFinding
The first phase reproduces.Over 08-11 to 09-03 the script finds 3.20 B tokens and $4,998 (1-hour writes), against 3.17 B and $4,974 published. The first census stopped at 19:04 UTC on 09-03; this one counts the whole UTC day.
Claude Code's own stats file counts every streamed record.Its totals are about 1.9× the de-duplicated count (found in the first phase, 5.94 B vs 3.17 B). The API bills per request, so the de-duplicated figure is the one used.
/usage shows plan utilisation, not dollars.Reaching a weekly cap says nothing about the dollar value of the tokens under it, and other projects shared the same allowance.
Not counted.Web searches and fetches billed per use, context compactions and title/summary side calls are not in the transcripts. In the first phase they came to about $13 in total.

The first phase's per-subagent and per-tool tables are in source-census.md. Its clearest cost lesson still stands: two 900-turn agents of 08-14 were left running through a credit pause and each re-read a very large context hundreds of times. An agent that has finished its brief should be stopped.