06 · Old 3DS
Old 3DS: the road to a real 60
On 08-11 the first feasibility study said this port was New 3DS only: the Old 3DS memory region could not hold it. Six weeks later the same game boots on an Old 3DS (268 MHz, no L2 cache), races at about 10 fps, and eleven days after that an internal development card holds 59–60 fps (245–256 of 256 frames on the first vblank) in Grand Prix races with the 3D slider up — on an internal development card, not the shipped v1.0.0 build. v1.0.0 now ships that same lever set as its default, but a separate real-hardware confirmation pass for the shipped binary's own 3D frame rate has not been done; see the note below and RELEASE-NOTES.md / STATUS.md for the current, authoritative status. This page is that road, card by card, with the number each hardware run actually logged.
[perf] preset = auto resolves to the Old 3DS's own tuned table (native renderer, NRR and stereo all on, plus the memory-fit levers card 4 still set by hand), so the same lever family card 4 ran by hand is now the default — see The native fork for how the per-console default resolves. What is not yet true: a separate real-hardware pass confirming the shipped v1.0.0 build's own 3D frame rate has not been run. The current, authoritative status is in RELEASE-NOTES.md, Known issues ("Stereoscopic 3D plays but isn't yet a locked 60 fps on either console") and STATUS.md's stereoscopic-3D row ("plays, not yet a locked 60 fps, exact hardware figure not confirmed for this release"). An explicit [perf] preset = off reverts to the legacy interpreter.The machine
| Old 3DS | New 3DS | Source | |
|---|---|---|---|
| ARM11 cores, clock | 2 × MPCore at 268 MHz | 4 × MPCore, up to 804 MHz | 3dbrew: Hardware |
| L2 cache | none | 2 MB ("optional" per 3dbrew) | 3dbrew: Hardware |
| L1 instruction cache | 16 KB per core; a miss costs ~37 cycles per 32-byte line (measured) | not measured here; misses can be served from L2 | hardware profile, below |
| Memory for the app | 64 MB application region (memtype=0); default heap grant 24 MiB | 124 MB app mode | boot log [mem-budget]; see Difficulties |
| Cores the port can use | core 0; core 1 is a 30% time-limited share (audio lives there) | core 0 + core 2 (render thread) | hardware profile, below |
[mem-budget] line: model=old3ds memtype=0 app=67108864 heapGrant=25165824.Every card, every number
A "card" is one SD-card round trip: a build, an ini, a written test plan, the user's play session, and the log back. The Old 3DS runs Luma 6.6 on arm9loaderhax, so the homebrew launcher does not start and every build went over as a CIA installed through FBI. There was no network loader for this phase; each row below is a physical trip.
| Date | Card | What it carried | What the hardware said | Log / doc |
|---|---|---|---|---|
| 09-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 |
| 09-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 |
| 09-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 hardware | hw-c4run1-0929 |
| 09-29 | C4 run 2: narrow audio lock | audio_ctx_narrow=1 | aud 25.8 → 0.2 ms; race wall 67.8 ms (14.7 fps), light scenes 40 ms | hw-c4run2-0929 |
| 09-29 | Wave 2: HOME + audio SIMD | HOME 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 silicon | hw-wave2-0929 |
| 09-29 | Wave 3: frameskip + O3DS preset | frameskip=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/s | hw-wave3-0929 |
| 09-29 | REL1 | All 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 boot | hw-rel1-fail, hw-rel1b-fail, hw-rel1crash-bisect |
| 09-30 | REL2 | Stock 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 main | hw-rel2 |
| 09-30 | REL3 | Every exact lever on, async file log, overlap kick | "no hitches, steadier"; steady mostly D=3 (20 fps rendered), rw50 24–36 ms | hw-rel3-run1, hw-rel3-run2 |
| 09-30 | HWPROF B | PMU 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 KB | hw-hwprof-B |
| 10-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 |
| 10-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 |
| 10-02 | n60-card2 | All 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 lap | hw-n60card2-V, hw-n60card2-C |
| 10-02 | n60-card3 | Never-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 |
| 10-03 | interim legacy 3D | card-3 build + [stereo] enabled=1 (NRR refused stereo, legacy path) | Black sky: the NRR sky consumed the section but could not draw under stereo | hw-interim3d |
| 10-03 | n60-3d | Stereo 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 fps | hw-n60-3d |
| 10-03 | n60-3d2 | Right 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 fps | hw-n60-3d2 |
| 10-07 | n60-3d3 | End-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 |
| 10-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 CPU | hw-n60-3d4 |
| 10-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 table | hw-n60-3d4b1 |
Phase 1 · Make it fit (09-24 → 09-29)
The Old 3DS milestone had sat unstarted on a stale note that said the user did not own one; corrected on 09-24. Four parallel subagents, then a fifth, worked under a simulated memtype=0 grant. The N64 RDRAM arena turned out to be 18% used and was halved to 8 MiB; audio_table moved off the heap behind a stream; texture uploads were capped. By 09-25 the canonical replay passed 49/49 under the simulated grant with 2.65 MiB of heap to spare. On 09-28 the real console answered memtype=0, byte-identical to the simulation, and the shipped build died in its boot preload exactly as predicted. One build flag, -DGDX_3DS_O3DS=ON (RDRAM 8 MiB plus a 16 MiB linear-heap pin, a 38.9 MB heap), became the Old 3DS profile; the New 3DS binary stays cmp-identical.
Phase 2 · Find the costs (09-29 → 09-30)
The first race spent 96 ms per frame with the GPU busy for 3.6 ms of it. The parallel wave that followed split the unattributed "other" bucket (it was the game thread waiting on the audio mutex while the HLE synthesiser held it: 25.8 ms per frame, cut to 0.2 ms by narrowing the lock), made the synthesiser 2.2× cheaper with bit-exact ARMv6 SIMD kernels (verified on the console, 0 mismatches), and found that the game was running in slow motion: one logic tick per drawn frame, Grand Prix at roughly 22–25% speed. Frame skipping (an automatic mode, then an even-cadence "steady" mode) put the game at full speed and made it "playable but nausea inducing".
A staged retained-mode renderer followed (per-category timers, GPU-resident machines, batched HUD/shadows/fx, a resident track with float24-safe rebasing, overlap of CPU and GPU), each lever exact against the interpreter. Every stage won in the emulator. On hardware, the third stage moved almost nothing: a steady race window stayed at 24–36 ms against the second stage's 26–35.
Phase 3 · The probe that changed the plan (09-30 → 10-01)
A performance-counter instrumentation pass put the ARM11 hardware counters in the build. The answer explained why the emulator wins did not transfer: the emulator does not model the instruction cache, and the renderer's cost on the console was mostly fetching code. The track, the one category whose levers had paid, was the one with the fewest I-misses per instruction: a few tight loops. The HUD ran 284 k instructions per frame at one I-miss every 17 instructions.
Two answers were on the table. A research pass (five lenses and an adversarial review) had concluded on 09-30 that a native 60 was "marginal for the 1P GP race" and proposed pose replay: re-submit the last frame with the skipped tick's poses ("60 Hz motion"). It was built, worked on hardware at 40 images per second, and showed sky jitter, tearing edges and broken particle trails; it was set aside. On 10-01 the user wrote "we should go for a real 60! i still believe it has to be possible."
Phase 4 · The native race renderer (10-01 → 10-02)
The renderer was redesigned around the cache, not around instruction counts. In races, each category (HUD, track, machines, sky, dressing, effects, shadows, decorations) runs one small specialised loop over a compact snapshot of game state and writes precomputed PICA200 command words directly into the GPU command buffer, drawing from GPU-resident meshes. The game's display-list sections for those categories are not built on rendered ticks at all. Each phase of the frame (core, one category, and the vendored citro3d path) is sized to fit the 16 KB L1-I: 9.1–11.0 KB per phase on card 2; the HUD phase reached 16,164 of 16,384 bytes on card 4. Game logic is untouched and checked by a game-state hash identical on every tick; pixels are held to a tolerance (2D: Δ>8 on ≤0.05% of pixels).
Phase 5 · 3D at 60 (10-03 → 10-08)
The first native-renderer builds refused stereo. The legacy path draws each eye per draw (in the emulator +0.77 ms per frame), which would have put back per-draw CPU work the native renderer had just removed. The fix builds the frame body once and resubmits the recorded command stream for the right eye, with the shader skewing clip x by a per-eye uniform and a per-draw stereo class (scene, anchored, deep sky, or none for UI). The emulator cost was +0.047 ms per frame against +0.77 for legacy per-draw stereo. On hardware the CPU fit, but the frames missed vblank: citro3d requests the swap only when the whole GX queue drains, and a whole right eye queued after frame end is a 5–7 ms tail. Interleaving the right eye per submitted chunk and swapping in end mode closed it: Mute City at ~60 in 3D on 10-07, Grand Prix race windows at 59–60 on 10-08.
What was still open at card 4, and what happened after
- Race end. The post-race results table cost ~40 ms per frame (147 HUD runs and 166 texture binds); still in race scope, so it tripped the cost guard at the time. Addressed in later work on the results screen.
- Heavier venues. Silence and Sand Ocean reached 16–18 ms of work on card 3.
- Menus stay on the interpreter with frame skipping; this is unchanged in the shipped release.
- Audio crackle on the Old 3DS: the leading hypothesis (a 3DS-vblank-vs-audio-rate mismatch) was measured and refuted — the N64 audio code is already a self-correcting feedback loop and the port's producer thread paces to ring depth, not vblank. A separate fix (the resample window alignment) shipped; the DSP-stage crackle candidate stayed open, and brief audio gaps during loads remain a known issue at release (see Overview).
- The bottom-screen framebuffer in VRAM crashed three cards' worth of boots (
lcd_vrammapped read-only for the CPU under the CIA's exheader, which the emulator does not enforce). Fixed and merged after card 4 (a crash-proof boot trail, a VRAM guard, and the fix itself); shipped off by default.
The Expansion Kit on the Old 3DS
The 64DD Expansion Kit build (course and machine editors, streamed disk image) was brought up on the New 3DS first. Asked whether it could reach the Old 3DS, measurement in the emulator under the Old 3DS profile found it fits with about 6 MB of heap to spare, and that gameplay under the native renderer is identical tick for tick on a ROM course (6,002/6,002) and a 64DD course (8,601/8,601). Two EK-only rendering defects (sky during the race intro, gadget and dirt-patch dressing) were found and fixed, with the shipped binary byte-identical before and after.
Fitting is not the same as playing. On release day the Old 3DS Expansion Kit build booted on hardware with the retail disk image streaming from the SD card, but the 64DD courses stuttered, the disk-streamed music broke up into static, and the course editor and menus, which still run through the general-purpose drawing path rather than the native race renderer, were very slow. The owner held it back: v1.0.0 ships the Expansion Kit for the New 3DS only, and the Old 3DS build stays a from-source experiment — see The native fork for the shipped release shape.