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.

~10fps
first Old 3DS race, 09-29 (wall p50 96 ms; the user said "like 9fps")
16.5ms
race median frame time on card 3, 10-02, 2D, 0 race hitches
59–60fps
3D Grand Prix race windows on card 4, 10-08, an internal development card (245–256 of 256 frames on the first vblank); v1.0.0 ships that same renderer family on by default — see the note below
0
bad draws in the hardware verify mode: 199,472 checked on card 2, and again (count not logged) on card 4
20
hardware cards and runs logged between 09-28 and 10-08
27
physical SD-card round trips logged for the Old 3DS campaign — no network loader on this console, so every test was a hands-on trip
The 59–60 fps 3D figure above is from card 4 (10-08), an internal development build with the native race renderer's stereo path turned on by hand — it is not a hardware-confirmed figure for the shipped v1.0.0 binary. What is true for v1.0.0: a no-ini [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 3DSNew 3DSSource
ARM11 cores, clock2 × MPCore at 268 MHz4 × MPCore, up to 804 MHz3dbrew: Hardware
L2 cachenone2 MB ("optional" per 3dbrew)3dbrew: Hardware
L1 instruction cache16 KB per core; a miss costs ~37 cycles per 32-byte line (measured)not measured here; misses can be served from L2hardware profile, below
Memory for the app64 MB application region (memtype=0); default heap grant 24 MiB124 MB app modeboot log [mem-budget]; see Difficulties
Cores the port can usecore 0; core 1 is a 30% time-limited share (audio lives there)core 0 + core 2 (render thread)hardware profile, below
3dbrew lists 128 MB of FCRAM on the Old 3DS in total; 64 MB is the application region the console grants this title, read from the console's own [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.

2D3D (slider up)
0204060cap 6009-29|boot · 2D race: 10.410.409-29boot09-29|run 1 · 2D race: 11.311.309-29run 109-29|run 2 · 2D race: 14.714.709-29run 209-30|REL2 · 2D race: 15.015.009-30REL209-30|REL3 · 2D race: 20.020.009-30REL310-01|REL4 · 2D race: 20.020.010-01REL410-02|card2 · 2D race: 57.357.310-02card210-02|card3 · 2D race: 60.060.010-02card310-03|3d · 3D race: 40.040.010-033d10-03|3d2 · 2D race: 60.060.010-03|3d2 · 3D race: 52.052.010-033d210-07|3d3 · 3D race: 60.060.010-073d310-08|card 4 · 3D race: 59.559.510-08card 4race frames per second (Grand Prix)
Race frame rate on the Old 3DS, per card. Values are the frame rates named in the run's log analysis: 1000 / median frame time where a median exists (first boot 96.0 ms → 10.4; card 2 14.2 ms → 57.3), the steady-mode rate (an early frame-skip preset → 15, a later one → 20 rendered) and the hold-1 share for the 3D cards (card 4: 245–256/256 → 59.5). Two bars are estimates from documented ranges: card 2's 3D (48–57 → 52) and card 3's 3D (Mute City only, ~60). The later cards skip drawing to keep logic at 60 ticks/s. Before the native race renderer the game ran in slow motion, so the first three bars are also the game's speed, not just its frame rate.
DateCardWhat it carriedWhat the hardware saidLog / doc
09-28Shipped build (RDRAM 16)The released New 3DS build, unchanged, installed as a CIADies 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-29First 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-29C4 run 1: O3DS profile-DGDX_3DS_O3DS=ON (RDRAM 8 MiB + 16 MiB linear pin: 38.9 MB heap), owned interpreter all=1 interp=1GP wall 88 ms (11.3 fps), TT/menu 43.7 ms; aud 25.8 ms = audio-lock wait, confirmed on hardwarehw-c4run1-0929
09-29C4 run 2: narrow audio lockaudio_ctx_narrow=1aud 25.8 → 0.2 ms; race wall 67.8 ms (14.7 fps), light scenes 40 mshw-c4run2-0929
09-29Wave 2: HOME + audio SIMDHOME bisect builds; audio_simd verifyHOME 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
09-29Wave 3: frameskip + O3DS presetframeskip=auto, preset=o3ds, gpuxf=1, DrawProbe calibration CIALogic 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
09-29REL1All 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
09-30REL2Stock codegen, frameskip=steady, HUD/machine/track retained pathsBoots. 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
09-30REL3Every 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
09-30HWPROF BPMU counters (c5pmu) and the r60 probeRender 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
10-01REL4+ pose replay (C6), compact HUD/sky emitter20 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-01n60-m1 (NRR milestone 1)Native race renderer: HUD, track, machines, sky; nrr_direct=2 verify runHard 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-02n60-card2All seven NRR categories (+ shadows, fx, dressing), GX-queue fixRUN 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
10-02n60-card3Never-blocking log, frameskip=race0, save fix, swap gate, texture fixes, HUD memo, native tunnel joins and decorationsRace median 16.5 ms, 0 race hitches; save persisted across a power cycle. User: "omg it ran amazing!"hw-n60card3
10-03interim 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
10-03n60-3dStereo on the NRR: one frame body, resubmitted for the right eye with a per-eye uniformNo 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
10-03n60-3d2Right eye interleaved per submitted chunk2D 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
10-07n60-3d3End-mode swap pacing in 3D, intro HUD fix, 1 Hz menu refresh fix, launch variantsMute 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-08n60-3d4+ track merge, per-draw diet, prewarm, HUD compile, FINISH mesh, lcd_vram=1Crash at boot on all three variants, no log: LCD framebuffers in VRAM, which the CIA maps read-only for the CPUhw-n60-3d4
10-08card 4 (VRAM fix)Same build, two levers off in the SD ini3D 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

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)

024616 KB · CPI: 1.061.0616 KB24 KB · CPI: 4.104.1024 KB32 KB · CPI: 5.625.6232 KBcycles per instruction, probe loop
The I-cache cliff, measured on the console. A hardware probe ran the same loop body at three hot-code sizes. At 16 KB (the L1 instruction cache) it retires about one instruction per cycle; at 24 KB it takes four. The game's own render path ran at CPI 4.34 with 43.6% of its cycles stalled on instruction fetch.

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).

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
Per-category CPU cost per rendered frame, before and after the native renderer. Hardware medians by category, before (10-01) and after (card 3, 10-02) the switch. Sum of these categories: 21.1 → 9.1 ms. Frame begin is left out because it measures different things in the two runs (GPU wait before, vblank slack after). Per tick, logic 1.51 → 1.58 ms is unchanged by design.

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

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.