Skip to content

claude/mk8d-64-2026-10-25-titles (2026-10-26): MK8D update + DLC on the 64-bit path, in-place RomFS, A32 base start-up faults

What changed

  • napple build --update-romfs in-place (development, --data-only only). Every version gets a romfs.map instead of a RomFS copy: the image as extents of the game files plus the AES-128-CTR parameters (key, IV high 8 bytes incl. the BKTR generation, counter offset). The runtime decrypts on read (CommonCrypto CTR). MK8D base + update + DLC data directory: 268 MB instead of ~19 GB (base 7.2 GB + patched 12.1 GB RomFS). LAS and SMO in place: ~250 MB each.
  • Source::map (napple-core io.rs, default false) resolves a source to host-file extents; implemented for FileSource (now remembers its canonical path), Slice, the CTR sections (nca.rs, bktr.rs), BKTR ExCtr (subsection generations) and Indirect (relocations). Compressed (NCZ) or in-memory layers return false -> the build fails with a clear message.
  • romfs_map::write_map verifies the map against the source (header, extent boundaries, 64 pseudo-random 12 KB ranges) before writing. MK8D update: 3281 extents.
  • Runtime: services/romfs_map.c (+ .h), 5-line hook in services/fs.c (StorageFile.map, used when romfs.bin and romfs.delta/ are absent).
  • The map holds content keys and absolute host paths: never ship it (the CLI refuses it without --data-only).
  • GetDisplayVersion (services/host_info.c): the NACP display version from ../napple-app.json when the data dir is the newest version (MK8D title screen now "Ver. 3.0.5" instead of "1.0.0"); other cases keep "1.0.0".
  • A32: main thread stack reserved for QueryMemory (a32/a32_proc.c, one line). The stack at 0x20000000 lay inside a Free gap, so the SDK's address-space scan placed the pl:u shared fonts (0x1100000 bytes at 0x1fe04000) over it: interpreter crash in nn::os::detail::SharedMemoryImplByHorizon::Map (store to the stack faulted). Happened in 1 of 1 interpreter runs before, 0 of 2 after.
  • tests/test_svc_mii.c follows the UUID v4 default Miis of 76249e5 (0..2 male, 3..5 female, "no name"); make test-services failed before.

Root cause of the old A32 start-up fault (main+0x562d00 / 0x822994, this = NULL + 0x3e74)

Bisected on this branch: it is the Mii default CharInfo fixed in 76249e5. With the old defaults (create id not a UUID v4) CharInfoAccessor::IsValid fails, MK8D never builds its Mii head-model singleton and the worker thread started at main+0x766f28 dereferences it. Restoring the old mii_core.c reproduces the fault in 2 of 3 runs at frame ~175; with the current one 0 of 13 lifted runs fault. Not time:u cmd 300 and not the module/TLR reservations (each disabled separately: no fault).

Measurements (Apple M5, headless, vsync on, NAPPLE_PERF_LOG=1)

Build Scene fps
MK8D v3.0.5 (A64, update+DLC) title, menus 44.9-45.4
MK8D v3.0.5 attract race / race 40-47
MK8D v1.0 (A32) lifted (a32jit.dylib) menus 44-45
MK8D v1.0 (A32) lifted race 43-45
MK8D v1.0 (A32) interpreter menus 37-45
MK8D v1.0 (A32) interpreter race 6-15
Frames in 90 s: lifted 3707-3799, interpreter 2340-2355. MK8D 120 s on the A64 path: 5362 frames.

The ~45 fps ceiling is gfx, not CPU: NAPPLE_GFX_PROF shows the guest blocked in SyncWait ~21 ms per present, per-submit commit->complete ~21 ms, while the present command buffer's GPU time is <= 8 ms and replay CPU 0.7 ms. Changing the headless pacing to a fixed grid (experiment, reverted) gave only 47 fps, so it is not just the headless sleep in the completion handler. LAS headless runs at 58 fps with the same code. Belongs to gfx (ac-render).

Verified

  • make -C runtime test test-services: all pass. cargo test -p napple-core -p napple-build: all pass (new romfs_map::tests::map_reads_back_like_the_source).
  • LAS 45 s headless (in-place RomFS): exit 0, 0 CRASH, 2647 frames. SMO 45 s: exit 0, 0 CRASH, 1921 frames.
  • MK8D update+DLC: 60 s, 120 s, 150 s, 180 s headless exit 0, title -> menus -> cup select -> races (screenshots).
  • MK8D A32 lifted: 13 runs (15-90 s), 0 crashes; the rare wild-pc crash was not reproduced.

Not done

  • VFP/NEON lifting (fallback ops 46/47 still interpreted): not needed for fps now, the lifted build already sits at the gfx ceiling.
  • iOS: nothing run. romfs.map is macOS/dev only by design (CommonCrypto exists on iOS too, but a shipped app needs a real romfs.bin).
  • The main checkout's uncommitted movie.c/time.c edits were not touched (the assignment forbids touching the main checkout); time.c is superseded by 76249e5; the movie.c hack (forces H.264 for every unknown decoder type) was not ported because MK8D did not need it.