Skip to content

AArch32 (32-bit titles, MK8D): CPU core, status and plan

Branch claude/exciting-davinci-unv0aa, Linux cloud session. MK8D does not run yet: only the CPU core exists. Nothing here is wired into the runtime build (runtime/Makefile does not compile src/a32/), so the Mac build is unchanged.

Why this is a separate feature

MK8D is AArch32 (NPDM says 32-bit; its NSO has 32-bit ELF dynamic entries, DT_REL, ARM/Thumb code). Apple Silicon cannot execute AArch32 and the whole pipeline (rewriter, loader, kernel, services) assumes AArch64 guest code at host addresses. So 32-bit titles need: a CPU core (done, below), a 32-bit loader/packager, a guest-address bias, an A32 svc ABI, HLE call marshalling (NVN etc.) and memory layout.

Done and verified (Linux)

runtime/src/a32/ portable C: a32.h (CPU state, hooks, a32_run), decoders (a32_decode_arm.c, a32_decode_thumb.c, one shared A32Insn form), executor (a32_exec.c: integer, branches, loads/stores, exclusives, CP15 TLS/counters, IT blocks), VFP (a32_vfp.c, a32_vfp_t.h: VFPv4 + half precision, ARM NaN/flush-to-zero/rounding semantics, vmov/vdup core transfers). Per-cpu decode cache keyed by pc and validated by the raw instruction bits.

Verification: runtime/tests/a32/fuzz.py (needs pip install unicorn) runs one random instruction on this core and on Unicorn/QEMU from the same random state and compares r0-r15, flags, memory, D registers, FPSCR. Results this session: ARM integer families, Thumb-2, and VFP data-processing/load-store/transfers show 0 mismatches except UNPREDICTABLE encodings (filtered in unpredictable()). Not compared: sticky UFC/IXC/IDC bits. Unicorn's CPU model lacks ARMv8 AArch32 additions, so these are unverified: vsel, vmaxnm/vminnm, vrint*, vcvt{a,n,p,m}, lda/stl/ldaex/stlex (use qemu-arm user mode, installed via apt, with a clang-built test program). Tooling in the container: clang targets ARM (clang -target armv7a-none-eabihf), lld, llvm-mc (use it to disassemble/enumerate NEON encodings).

Not done

  • NEON: a32_neon.c is a stub (returns UNIMPL). A large draft (3-same, 3-diff, scalar, shifts, modified immediate, two-reg-misc, vext/vtbl/vdup, all via a32_neon_f32, which does not exist yet) is in runtime/src/a32/wip/a32_neon.c.wip; its structure load/store part is unfinished scaffolding. The two-register-misc opcode map was enumerated with llvm-mc and is encoded in the draft.
  • IT blocks, lda/stl, v8 VFP, exclusives across threads: written, only checked single-step or not at all. Add whole-program tests: compile C with clang for armv7a/thumbv7a, run under qemu-arm, compare with this core (hooks: svc, exit).
  • Everything outside the CPU core (below).

Plan to MK8D

  1. NEON (finish draft, fuzz neon_dp, neon_ls families; add NEON structure loads/stores).
  2. Program-level tests against qemu-arm (IT, calls, floats, atomics).
  3. SVC ABI: Horizon A32 register layout. Algorithm (from Atmosphere svc_codegen_impl_layout.hpp, checked against libnx stubs): stage 1 is AAPCS over the C prototype with outs counted as pointer args, 4 argument registers on A32, 64-bit values aligned to even registers, overflow to the stack; stage 2 maps register args to the same svc register, stack args to the first free register (64-bit = two chunks), outputs: Result in r0, other outs in the first free output registers. Examples: CreateThread in r1,r2,r3 + priority r0, core r4, handle out r1; WaitSynchronization handles r1, n r2, timeout r0:r3, index out r1; WaitProcessWideKeyAtomic timeout r3:r4; WaitForAddress value r2:r3, timeout r4:r5; GetInfo id1 r0:r3, out r1:r2. Signatures: Atmosphere svc_definition_macro.hpp. Adapter converts to the A64 register layout and calls svc_dispatch. Needs custom handling: QueryMemory (struct), GetInfo address results (subtract bias), GetThreadContext3.
  4. Guest address bias: A32 guest memory is a 4 GiB window at host BIAS (>= 4 GiB, so raw A32 addresses are < 2^32). Make guest_ptr() add the bias to addresses < 2^32 (IPC descriptors then work unchanged); the svc adapter pre-biases pointer args for handlers that dereference raw (futex words). A32 TLR must live in the guest window (tpidruro = guest address; TSD runtime_keys[2] = biased pointer).
  5. Memory: A32 process layout (Horizon 32-bit: ASLR ~0x200000..0x40000000, heap 0x40000000.., alias, stack region); replace the memory svc handlers for A32 instead of editing memory.c.
  6. Loader/packager: Elf32 dynamic parsing in napple-build (tables.rs), R_ARM_RELATIVE/GLOB_DAT/JUMP_SLOT/ABS32; skip the AArch64 rewriter; napple-compat already flags 32-bit NPDM as RISK. Test with a clang-built ARM shared object wrapped as an NSO.
  7. Threads: threads.c thread_main -> an A32 entry (a32_run loop) instead of runtime_thread_enter; main.c boot calls nn::init::Start via the core.
  8. HLE imports: NVN functions are host functions returned by nvnBootstrapLoader. For A32 return addresses in [hle_lo,hle_hi); the hle hook marshals AAPCS args (r0-r3 + stack; 64-bit pairs; pointers + bias) into the host function. The signatures are all in gfx/src/nvnapi/nvn_protos.h (generated typedefs): generate a signature table from it. Structs with pointers differ between ABIs (e.g. NVNshaderData) and need custom shims. Float ABI (hard vs soft) of Horizon A32 is unknown: assume hard-float, make it a switch.
  9. Performance: this is an interpreter (decode cache). MK8D at speed will need an A32->A64 translator over the same A32Insn (AOT in napple-build or a JIT on macOS; iOS allows only AOT). Do not start before the interpreter boots the game.