08 · 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. This page is the war log: each fight, its root cause, and what it took to win.

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 pointers, 32-bit address spaceEvery "small address means N64 offset" heuristic in LUS and the bridge misfired: the image sits at 0x100000 and the heap at 0x8000000, both inside the N64's 8 MB RDRAM range.
124 MB app region, 64 MB on Old 3DSOld 3DS formally dropped in week one (revisited from 09-24; see below). Assets prebaked into o2r archives; ROM never touches the console.
PICA200 fixed-function GPUNo fragment shaders. Every N64 color-combiner mode mapped to at most 3 TexEnv stages (combiner census, 2026-08-12). Fog goes through a depth-indexed LUT that F-Zero X's depth range defeats.
Rotated 240×400 framebufferEverything renders portrait and is rotated by a fixup matrix. Viewports, scissors, and the stereo frustum shift all live inside that matrix.
Three cores, one of them yoursCore 0 runs the game, core 1 is fractional (APT_SetAppCpuTimeLimit), core 2 exists only if Luma grants the kernel cap, core 3 belongs to the OS.
No debugger, no stdoutReceipts written to a filelog ring buffer on the SD card, Luma exception dumps symbolized against the ELF, svcOutputDebugString only visible in the emulator.
Azahar is not a 3DSThe emulator's dynarmic JIT and host CPU made it slower than hardware on menus and faster on others, could not time-slice core 2 honestly, and had a stale-frame screenshot oracle. Hardware was the only truth; 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.

Explosion flicker: CCMUX 11

Ship explosions spammed "Unsupported ccmux 11". That is G_CCMUX_SHADE_ALPHA in the C slot: debris is (1−SHADE)·SHADE_ALPHA+SHADE, a white-hot flicker. Mapped, plus a dead-cycle skip, which made the combiner translation total across the game.

Menu ships 1.6× too large

Found by the user on 09-03 with side-by-side screenshots against the real game. F-Zero X translates a full-size 320×240 viewport onto each grid cell; the backend clamped and unsigned-cast the negative rectangles. Fix: fold any non-full-window viewport into the per-draw projection (NDC scale and offset, scissor intersection, hor+ x-scale). Killswitch vpfix. Fixed by the orchestrator after the assigned agents died on API overloads.

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.

Lifecycle: HOME, sleep, close

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.

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.

Process failures, honestly

Chapter 2 · What the Old 3DS fought back with (09-24 → 10-08)

The Old 3DS added a slower CPU, no L2 cache, a 64 MB application region, no second render core, and a console that only took CIAs. Every fix below came from a hardware log; the per-card numbers are on the Old 3DS page.

FightSymptomRoot cause and fixSource
A stale noteThe Old 3DS milestone sat unstartedAn earlier note had said the user did not own an Old 3DS; corrected on 09-24 and recorded so it cannot resurfaceproject memory
No network loaderThe Homebrew Launcher will not startLuma 6.6 on arm9loaderhax; every build is a CIA installed through FBI, every test a physical SD trip (27 log sets this phase)hardware logs
Out of memory at bootoperator new failed size=175844RDRAM arena 16 → 8 MiB, streamed audio_table, upload cap, 16 MiB linear pin. Also: a reported "free" figure turned out to be the free list, not real headroom (~2.2 MiB real, not 126 KB)boot logs, simulation
HOME freezeHOME froze the game image; power-off neededNot the audio driver, not the resource file system, not core 1 (probe apps ruled each out). The bottom-backlight display session was held open for the process lifetime; the HOME menu blocked on it. Open/call/close per toggle; 7/7 resumes cleanhardware probes
Slow motionRaces felt wrong even at 15 fpsOne logic tick per drawn frame: Grand Prix at ~22–25% speed. Frame skipping; the first automatic mode was "nausea inducing", a steady even-cadence mode held a consistent paceframe-skip measurements
Draw-distance pop-in"track pop-in jarring"An aggressive draw-distance setting in the Old 3DS preset; removed from the preset the same dayhardware report
Cold, size-optimised boot crashFirst release candidate crashed after graphics setup, no dumpFour-title bisect: size-optimised code crashes cold, stock and balanced-optimisation plus hot-path ordering both boot. Code generation reverted to stock; the underlying bug was never foundbisect card
Hitches"lots of awkward hitching"Synchronous SD log flushes on the main thread (26–270 ms), later a full ring buffer forcing 1.1–2.8 s writes. An async writer, then a log that drops instead of blocking, fixed itframe-time logs
Emulator wins that did not transferAn interpreter-tuning pass that won big in the emulator barely moved on hardwareInstruction-cache misses (CPI 4.34). The emulator does not model the I-cache; CPU performance counters became the judge insteadhardware profiling
A coordinator's own numbers were wrongAn early per-category baseline ("track 14.8, HUD 12.8") didn't hold upThe aggregator tagged windows by the wrong frame counter: those were warm-up frames, not steady-state. Corrected in the next research passresearch notes
60 Hz pose replay shelvedSky jitter, black tearing edges, giant effect trianglesRe-submitting the last frame with new poses cannot fix per-tick vertices (sky, effects). Set aside in favour of a real 60design notes
Verify-mode freezeAn early native-renderer build froze at race startA state save/restore step copied the GPU's live command queue along with everything else, rolling back a completion the hardware had already acted on. The emulator's equivalent is synchronous and could not show ithardware-only repro
Saves never persistedRecords lost after the first saveThe SD card's rename fallback triggers only on one specific error code; the SD archive used here answers a different one. A crash-safe replace with boot recovery fixed it; a save survived a power cycle on the first card built with the fixsave-path investigation
Black sky in 3DAn interim stereo buildThe native sky path consumed its section but could not draw under stereo, and lacked the refusal the other categories had. Rule adopted: a category that consumes a section must draw it or refuse up frontrender-path notes
3D missed vblank3D ran at ~40, then the 40s–50sThe graphics API requests the screen swap only when the whole command queue drains; a whole right eye queued after frame end is a 5–7 ms tail. Interleaving per chunk and swapping in end mode fixed itstereo investigation
Bottom-screen VRAM boot crash"all 3 crashed", random pixels, no logFramebuffers moved to VRAM, which the installed title's permissions map read-only for the CPU; the first bottom-screen write aborted. The emulator does not enforce that permission, so it never showed up there. Fixed and shipped off by defaultboot-crash investigation
Results tableRace-end screens ran heavyAround 40 ms per frame of HUD draws and texture binds while still counted as race scopeprofiling

Process failures, honestly

Chapter 3 · What shipping fought back with (10-08 → 10-19)

FightSymptomRoot cause and fixSource
A real memory ceiling, reproduced under simulationA forced run on the Azahar emulator, under the real Old 3DS memory budget, died mid-race on a routine asset loadReproduces with every feature lever on or off, ruling each one out as the cause; a clean stock-build re-test could not be completed on real hardware. A separate real-hardware capture (heapUsed≈24.2 MB peak) suggests the margin is thin on real hardware too, though the crash itself has not been reproduced there. Left as a known limit, not fixed structurally before releaseforced-budget hardware simulation, Azahar emulator
A plausible audio bug that wasn't oneOld 3DS audio crackle, with a specific rate-mismatch theory on the tableThe N64's own audio buffer sizing is already a closed-loop rate tracker, and this port's audio producer thread paces to ring depth, not to the console's refresh rate — both facts rule the theory out on inspection, and re-reading the original measurement showed the "crackle rate" was one boot-time transient averaged over a whole race, not a sustained rate. A smaller, real fix (resample window alignment) shipped insteadre-analysis of existing logs, decomp source
A real console froze, with nothing to readA New 3DS hung every time at one title-screen demo, no crash dumpAn undersized shared GPU command buffer — see The Native Fork for the bisection, root cause and fixhardware bisection, see The Native Fork