AArch32 (MK8D): measurements and recompiler plan (Mac session, branch claude/aarch32-recompiler-2026-10-25, based on claude/exciting-davinci-unv0aa 74c7d30)¶
Measured on the real game (Mario Kart 8 Deluxe base v0, napple build --dump-exefs)¶
- NPDM flags 0x04 = 32-bit instruction set, 32-bit address space. 8 modules, all ELF32 (Elf32_Dyn, DT_REL): rtld, main, sdk, subsdk0..4.
- Code: main 10.8 MB, sdk 3.7 MB, subsdk0 1.1 MB, subsdk4 3.2 MB, others small: ~19 MB of mixed ARM/Thumb-2 (about 5M instructions).
- Relocations: only R_ARM_RELATIVE (23), ABS32 (2), GLOB_DAT (21), JUMP_SLOT (22). main: 112,778 RELATIVE + 2,136 ABS32 + 756 GOT/PLT. RELATIVE targets include function pointers and vtables, which gives hundreds of thousands of code entry points for discovery.
- Init arrays: main 722 entries, sdk 76, subsdk0 24, subsdk4 46. GNU-hash modules (no DT_HASH): dynsym size from the NSO header (0x98/0x9C), as the A64 tables already do.
- Today
napple buildfails on this game at the A64 rewriter (A32 words decoded as A64), then at tables (ELF32 dynamic table): see side-session note mk8d-is-aarch32.md. - Verified on this Mac:
runtime/src/a32/*.ccompile warning-free with the system clang; Apple clang targetsarmv7a-none-eabihf(test programs can be built here).
Architecture recommendation¶
Every approach needs the same system pieces; the CPU core is the only part that differs. So: bring-up first on the existing interpreter, then the recompiler over the same decoded form.
1. Bring-up (interpreter): Elf32 tables + relocation (crates/napple-build tables.rs), A32 module loader, guest-address bias (4 GiB window), A32 svc adapter (layout algorithm in the aarch32-core note), A32 thread entry, NVN HLE marshalling (nvn_protos.h), NEON. Gate: MK8D reaches nnMain and the first nvn calls.
2. Recompiler (AOT, iOS-compatible): lift A32Insn (already decoded for both ARM and Thumb, including IT conditions) to A64 blocks emitted by a new napple-rewrite mode: guest registers live in the A32Cpu struct (later pinned in x19-x28), flags in NZCV when safe, loads/stores through the bias register, calls/returns and indirect jumps through a translation table; ARM/Thumb mode is part of each entry point (bit 0 of the address). Code discovery: symbols, RELATIVE/ABS32 relocation targets, init arrays, linear sweep of reachable code; anything not translated falls back to the interpreter (so correctness never depends on discovery being complete). Hot loops in VFP/NEON first.
3. Keep the interpreter as the oracle: differential-test every translated block against it (same fuzz harness, runtime/tests/a32/fuzz.py).
Status¶
Plan only; no code changed on this branch yet beyond the base.
Done and verified on the Mac (MK8D base v0)¶
crates/napple-build/src/tables32.rs: ELF32/DT_REL modules are detected (is_elf32) and packaged without the A64 rewriter: plain image (<name>.patched.bin) + NTB2 tables (same record layout as NTB1, magicNTB2, ARM relocation kinds 2/21/22/23, REL addends read from the image).napple build --data-only --updates nonenow completes for MK8D (7 modules, 6.8 GB romfs.bin; rtld is not packaged, as for A64 titles). Unit tests:cargo test -p napple-build tables32(2 pass).runtime/src/a32/a32_module.{h,c}: 4 GiB guest window (host reservation,host + guest), module mapping at guest bases from 0x200000, REL relocation, symbol hash, cross-module import resolution, HLE stub addresses at 0xF0000000+ for imports nothing defines. Host testruntime/tests/a32/test_module.c:cc -std=gnu11 -O2 -o test_module tests/a32/test_module.c src/a32/a32_module.c && ./test_module <data dir>. Result on MK8D: 161,806 relocations (RELATIVE 123,152, ABS32 27,051, GLOB_DAT 1,854, JUMP_SLOT 9,749), 12,197 imports resolved across modules, 0 left for HLE, 38 weak-undefined = 0, 869 of 869 init-array entries inside module text.
Next (in order)¶
Wire src/a32 into runtime/Makefile; A32 svc adapter + guest-window memory handlers; A32 thread entry and boot (nn::init::Start via the interpreter); NVN import marshalling (nvnBootstrapLoader returns host functions); NEON; then the AOT translator.
Interpreter bring-up on real MK8D code (host harness runtime/tests/a32/a32_boot.c, mock svc layer)¶
cc -std=gnu11 -O2 -Isrc/a32 -o a32_boot tests/a32/a32_boot.c src/a32/a32_exec.c src/a32/a32_decode_arm.c src/a32/a32_decode_thumb.c src/a32/a32_vfp.c src/a32/a32_neon.c src/a32/a32_module.c && ./a32_boot <data dir> [million instructions]
- MK8D's SDK predates nn::init::Start: the loader stub (rtld) is the entry. Its main-thread sequence, read from its code and PLT: __nnDetailInitLibc0, nnosInitialize(main thread handle, ...), __nnDetailInitLibc1, nndiagStartup, [nninitInitializeAbortObserver], [nninitInitializeSdkModule], nninitStartup, __nnDetailInitLibc2, module constructors, nnMain, [nninitFinalizeSdkModule], nnosQuickExit. The harness drives exactly that.
- Result: libc init, nnosInitialize, nninitStartup (SetHeapSize) and __nnDetailInitLibc2 run to completion and 706 of main's 722 constructors (~290M guest instructions) execute; the first stop is a Break caused by the mock kernel failing ConnectToNamedPort/SendSyncRequest (service manager), i.e. the real HLE kernel/services are the next integration step.
- Interpreter bugs found by running real code and fixed: ARM ubfx/sbfx decode mask (bit 20 is the top bit of widthm1: half of all encodings decoded as undefined); mrrc p15,0,..,c14 (CNTPCT) now supported like CNTVCT.
- NEON implemented: the draft was completed and made the real a32_neon.c (the structure load/store, VLDn/VSTn multiple/single/all-lane, was rewritten) and a32_neon_f32 (standard-FPSCR float semantics, estimates, compares, conversions) was added to a32_vfp.c. Unverified: not fuzzed against Unicorn/qemu yet (only exercised by MK8D's libc memset/memcpy paths); a32_neon_f32 is new and untested; NEON structure loads have no alignment checks.
Next¶
Wire src/a32 into runtime/Makefile and the real kernel: A32 svc adapter (register layout in the aarch32-core note), guest-window memory handlers, TLR in the window, A32 thread entry/threads, IPC buffer pointers with the guest bias; then NVN marshalling (nvnBootstrapLoader); fuzz NEON; AOT translator.
MK8D runs on the Mac (interpreter), 2026-10-25¶
napple-runtime <MK8D data dir> --headless --seconds 280 boots Mario Kart 8 Deluxe through the real kernel/services and renders its title screen (242 frames, ~15k draws in the run: "MARIO KART 8 DELUXE" logo, lens flares; screenshot via NAPPLE_SCREENSHOT=<prefix> NAPPLE_SCREENSHOT_FRAMES=60). Loading is CPU bound at interpreter speed (several threads decompress assets with an LZ-style loop at ~250M instr/s per thread); the AOT translator is the next lever.
How the 32-bit process is wired (all under runtime/src/a32/, hooks in main.c, memory.c, ipc.c, threads.c, signals.c):
- 4 GiB guest window at a host base (macOS kills arm64 processes with PAGEZERO < 4 GiB, so identity mapping is impossible); memory_init_a32 lays out modules < 0x10000000, stack region 0x10000000, heap 0x30000000 (2 GiB cap, MK8D asks 0x78000000), alias above. guest_ptr() accepts 32-bit guest addresses and window host pointers; IPC buffer descriptors are converted to host addresses when parsed and back when answered.
- a32_proc.c: svc adapter (register layout derived from each svc's C prototype with Atmosphere's algorithm; signature table; pointer args +base, address results -base; QueryMemory keeps the 40-byte MemoryInfo, main-thread stack reported as Stack), a guest thread = a pthread running the interpreter (all kernel waits/IPC/futex code unchanged), TLR in the window, rtld's startup sequence replayed on the main thread (this SDK has no nn::init::Start), NVN version detected from the ARM compare (52.32 for MK8D).
- a32_hle.c + a32_nvn_sigs.inc (generated by tools/gen_a32_nvn.py): nvnBootstrapLoader is overridden like for 64-bit titles; every NVN entry point returns a stub; marshalling follows the 32-bit AAPCS (hard float); shims for the three calls with arrays of pointers / pointer-sized struct fields (nvnWindowBuilderSetTextures, nvnCommandBufferSetRenderTargets, nvnProgramSetShaders, nvnMemoryPoolMapVirtual); unknown names (nvnDeviceWaitForError) get a logged no-op stub, never NULL. Callbacks (SetMemoryCallback, debug callbacks) are passed as NULL: not supported yet.
- Services: fsp-srv 203 (OpenPatchDataStorageByCurrentProcess: not found), acc IManagerForApplication 130 (stub), audio renderer wave/pool addresses through translate (audio/src/mix.c reads voice data via it), svc 0x5F no-op. The title needs the firmware's system data: build with napple build ... --firmware Firmware.zip (MiiModel 0100000000000802 is mounted at start).
- GPU: rendering into a block-compressed texture through an integer view (runtime texture compression) draws into an auxiliary block-grid texture; the copy back into the BC texture is NOT done yet.
Debug env vars: NAPPLE_A32_TRACE=1 (single-step ring + guest abort strings), NAPPLE_A32_CALLS=1 (last 1024 call targets in crash reports), NAPPLE_A32_WATCH=1 (where busy threads are), NAPPLE_A32_NVNTRACE=1 (every NVN call), NAPPLE_A32_DUMP=1. The crash report prints the AArch32 state and a heuristic stack backtrace.
Known gaps: no input verified for MK8D, audio not heard, no copy-back for compressed render targets, NEON and a32_neon_f32 not fuzzed against a reference, interpreter speed, MK8D v1376256 update and DLC not exercised (data dir built with --updates none), iOS (window base differs) untested.