Outdoors in Breath of the Wild (the user's Great Plateau save, Mac M4 Pro) a frame had about 386 render passes and loaded and stored about 900 MB of attachments, which an iPhone at 60 fps can hardly move (about 55 GB/s). 220 of those passes ended only because the game updates the first 64 bytes of a 64 KB vertex uniform block before each draw group (nvnCommandBufferUpdateUniformBuffer inside the pass) while its shaders read up to 544 bytes of it. A deferred update that covers only part of what a draw reads used to end the pass. - Partly covered blocks (nvn_mtl.m ubp_get): the shadow starts from memory and takes every inline update queued in the submission, newest last, incrementally per range. It is refused, and the pass ends as before, when something else may have written the range by the time the pass runs: a copy, fill or feedback write queued in the submission, a storage buffer a shader may write or a texture->buffer copy over it (sbw, by 4 KB page), or a GPU write or counter report of an earlier submission still running (gw). A counter report of the same submission does not count: the host writes reports on the CPU after the whole submission completes (Fire Emblem Engage writes its reports into a uniform block's page). DM_NVN_UB_PARTIAL=0 turns it off; DM_NVN_UBP_CHECK=1 lands the updates, waits for the GPU and compares every shadow with memory. - cb_extent.tsv (new tool dm/gfx/tools/cb_extent.py, c1_guard.py's interval arithmetic over the translated MSL): per shader, how far into each uniform slot it can read, and the storage slots it never writes. Like c1_full.tsv, the host loads it only for its chain version; build_ipa.sh copies it, the NSP script writes it for provisional profiles, and the chain procedure regenerates it. Without it bindings are copied whole (up to 64 KB). Ten profiles have one. - Route B's reflection never says whether a storage buffer is written, so every one read as read-only and its writes went unnoticed: a storage buffer now counts as written unless SPIRV-Cross typed it const (nvn_refl.m sbuf_rw) or cb_extent.tsv proves the slot unwritten. - Lazy clears (lc_*): a set of targets that gets only clears before the game sets other targets no longer opens a pass just to store them; the next pass that attaches the same texture level and layer through the same view clears it as its load action, and the rest land as before before anything else could see them. Breath of the Wild's four clear-only passes before the G-buffer fold into its first pass. Off in TBDR mode; DM_NVN_LAZY_CLEAR=0 turns it off. - The residency set line counts shadows from memory, refusals by reason, and lazy clears. On the Mac, old (DM_NVN_UB_PARTIAL=0 DM_NVN_LAZY_CLEAR=0) and new side by side on each game's route; attachment loads + stores per frame: - Fire Emblem Engage battles: about 1,850 -> 360 MB, 88 -> 38 passes. - Tears of the Kingdom: about 1,100 -> 700 MB, 124 -> 116 passes. - Breath of the Wild outdoors: 386 -> about 158 passes, GPU p50 about 9.3 -> 6.5 ms, submit 5.7 -> 4.2-4.9 ms; where the old host fell to 30 or 20 fps under load, the new held 60. - Link's Awakening, 13 Sentinels: about 15% less. Super Mario 3D World: 5% less (lazy clears). - Odyssey, Three Houses: 2-8% less. Mario Wonder, Metroid Dread: unchanged. No new red lines in any of them. The shadow check found every shadow equal to the GPU's memory (BotW 346,000, Engage 284,000, TOTK 86,000), and t_nvn passes 68/68. Screenshots differ only by timing. Tried and dropped: DM_NVN_TBDR=1 (it turns the deferred updates off), DM_NVN_BARRIER=game (no gain), and same-target continuation outside TBDR mode (BotW's decals end the pass anyway). What still splits BotW's big passes: the GPU particles' compute dispatches (about 190 MB a frame) and decals that sample the G-buffer. Mac shader libraries were built for the current chain of TOTK, Engage, Wonder, Dread, Odyssey, Link's Awakening, Three Houses and 13 Sentinels (c1_full.tsv headers already matched). The IPA ZeldaBreathOfTheWild-2026-10-21-passes.ipa (an earlier state of these changes plus the uncommitted frame-step probes) awaits an iPhone test.
Porting a Switch title: agent guide
These pages are for an agent asked to analyse a Switch NSP and port it with the native-execution approach of this repository and d2r-mobile: the game's own aarch64 code runs natively on Apple hardware, a rewriter patches only the instructions Apple's kernel cannot run, the host replaces Horizon system services, and NVN is reimplemented on Metal.
Everything here was learned on Diablo II: Resurrected (SDK ~12, NVN 53, the original target of d2r-mobile) and Super Mario Bros. Wonder (SDK 16.2.0, NVN 55, this repository; its update 1.2.1 uses SDK 16.2.12), then extended by Fire Emblem Engage (Unity, SDK 12.3.7), Metroid Dread (MercurySteam's engine, SDK 11.4.3) 13 Sentinels: Aegis Rim (Vanillaware's engine, SDK 11.4.3, the same sdk and movie module as Dread) Super Mario 3D World + Bowser's Fury (Nintendo EPD's sead / agl engine, SDK 10.4.0, NVN API 53) The Legend of Zelda: Breath of the Wild (EPD's sead / ksys engine, SDK 20.5.22, DT_RELR relocations, update 1.9.0), The Legend of Zelda: Link's Awakening (Grezzo's engine on NintendoWare, SDK 8.2.0, lower-left window origin) Hyrule Warriors: Age of Calamity (Koei Tecmo's KTGL, SDK 9.6.0, RDB archives, shaders as GLSLC outputs) Super Mario Galaxy (a Wii game on NERD's Hagi layer: recompiled PowerPC code, SDK 18.3.5, Vulkan with SPIR-V generated at run time, a 2.9 GB bss), Ace Attorney Investigations Collection (Unity 2022.3 IL2CPP, SDK 17.5.3: the C# file API through the SDK's C library), and the bring-up of Octopath Traveler 0 (Unreal Engine 5, SDK 19.3.13: an AES-encrypted pak, Oodle-compressed shaders that the game's own code decodes on the Mac, its allocator replaced by the host's). Where titles differed, each is given; the next title will differ again, so treat every number below as "measure it, don't assume it".
| Page | Covers |
|---|---|
| 01-extract.md | Keys, NSP → NCA → ExeFS / RomFS, update NSPs, the workspace layout |
| 02-survey.md | Static survey of a new title before touching the host: SDK version, imports vs. host hooks, SVCs, instructions to rewrite, NVN surface, data containers, Ghidra / REA setup |
| 03-bringup.md | Getting the host to boot the game: the run loop, how to read the log, and a catalogue of every failure pattern met so far with its root cause and fix |
| 04-graphics.md | NVN on Metal for a new title: object sizes, shader containers and keys, binding layouts, bindless textures, depth mode, index sizes, swizzle, transient render targets, the diagnostic switches |
| 05-debugging.md | Recipes: crash reports, lldb scripts, Ghidra headless decompilation, objdump, memory growth, render-pass and texture dumps, on-device diagnostics, offline pipeline link checks |
Ground rules
- Bring your own files. Keys, NSPs and anything extracted from them stay in the game's work folder (
$W=<NSP folder>/<title>-work, next to the NSPs on the user's external drive; the home folder is nearly full), never in the repository, never in a commit, never printed. Print key names and lengths only. - Measure, then change. Each fix in this repository started from a concrete observation (a crash address and its decompilation, a census over all shaders, a memory profile). Write the evidence into the code comment next to the fix, the way the existing code does; the next title will need it.
- One repository, one profile per title. nxnative (first called smbw-mobile) started as a copy of d2r-mobile's generic parts. Title- or SDK-specific constants live in the game profile
dm/games/<id>/(dm/games/README.md). That covers program ID, known builds, modules, address-space partition, NVN driver tables, the shader tables and the app identity. The code reads the profile throughgame_profile.h(host) ordm/games/game.py(tools). Behaviour that fits only one title's code is gated by that title's build ID (dm_main_build()), never left on for every title. Some things are still built into the translator: the texture / sampler handle bases and the depth mode (see 04).dmsh_build.pyrefuses a profile whose bases differ from the translator's. - Write code comments in English, and start log lines that mark something unimplemented or wrong with
[RED]. - Test where it runs. The Mac host (
dm/host/dmhost) is the development loop; the iOS app reuses the same host and graphics code. Never claim something works on iOS without running it there.
Checklist for a new title
-
Run
scripts/build_ipa_from_nsp.pyon the title's NSP folder. With no profile for its title ID, it writes<NSP folder>/<title ID>_survey/survey.md:- modules and build IDs, SDK version and components, engine and graphics API markers;
- RomFS containers;
- the rewrite-site census and imports vs. host hooks.
It extracts only the code modules plus a RomFS list for the survey. Unless given
--survey-only, it then writes a provisional profile into<NSP folder>/<title ID>_build/profile/<title ID>/, extracts the game, boots it on the Mac until its heap request when it has virtual address memory (the heap check) and packs a best-effort IPA. That profile is the starting point for step 3: the static keys follow rules checked against every profile, the rest are guesses or defaults (dm/games/README.md); the run-time keys (render_cond,tex_flush_upload,core_preempt, …) are the bring-up's to find. For Mac runs, exportDM_GAMES_DIR=<NSP folder>/<title ID>_build/profileuntil the profile moves intodm/games/. -
Extract base (and update, if in scope) — 01. Finish the survey — 02. Write down: SDK version, NVN API version, module list, imports the host lacks, SVCs, instruction census, NVN loader pattern, shader containers.
-
Add a game profile
dm/games/<title ID>/game.tsv, named after the base game's title ID (start from010015100B514000/game.tsv: identity,buildrows,modules; keep the memory / NVN values until they are measured) and build the host for it withmake -C dm/host GAME=<id>. -
Run the rewriter and check its counts against
insn_census.py. -
Boot on the Mac and iterate on the failure catalogue in 03 until the game reaches its frame loop.
-
Extract and translate the shaders, find the binding layout by census, and get the first frame — 04.
-
Check memory over a minute of running (
footprint, the host's memory report line); fix growth before moving on. -
Input, audio, then the iOS bundle (d2r-mobile's
dm/ios, RomFS inside the app). -
Test on a phone: an unsigned IPA the user sideloads, switches through
env.txt, results throughDocuments/log.txt(05). Several bugs only showed there (the command-buffer byte and the audio buffer size in 03, pipeline link failures in later levels in 04). -
For a game update: extract it over the base game, diff the imports, carry the per-build addresses over, and rebuild the shader tables for both versions (01).