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.