Files
nxnative-easy/docs/agents
seeddsandv0lumehi 76545a58a7 Mac: each controller is a player of its own, for local co-op (GitHub issue #74), from PR #84
Every GameController fed the same input path (front_mac.m), so the last one moved won, and hle_hid.c reported
exactly one npad whatever the game supports. Now the first controller is player 1, with the keyboard, the next
player 2 and so on up to 8 (pad_slots.h; a removed controller frees its player and gets it back when a
controller of the same name reconnects). Player 1 stays on No1 docked and Handheld in handheld mode, player n is
on No n (No1 stays empty in handheld mode: games take Handheld and No1 both for player 1). The game's supported
npad ids decide how many play: a one-player game ({No1, Handheld}) gets every controller as player 1, a
controller past a game's last player is not used, as on the Switch. A connect or disconnect signals that npad's
style-set event through hle_signal_bound_system_event (hle_thread.c), which signals only an event still bound,
from the macOS main thread where the notifications arrive; player LEDs follow the Switch's patterns; the
controller-support applet reports the connected players, capped at the game's maximum; only player 1 shakes.
DM_PAD_MERGE=1 merges every controller into player 1 as before; DM_KBD_PLAYER=own makes the keyboard player 1 on
its own. One controller, or the keyboard alone, behaves as before. iOS is unchanged: DmGamepad.m still feeds
player 1 and never reports further players.

Rebased onto a678703 (front_mac.m's includes, the Input row of docs/development.md). Checked: host unit tests
pass with the new t_hid (44 checks: slots and reconnects, npads docked and handheld, one- and four-player games,
events, LEDs, the applet); the Mac host builds for Breath of the Wild and Mario Kart 8 Deluxe with no new warning;
hle_hid.c and hle_thread.c pass an iOS SDK syntax check; Hades in a window with the keyboard and DM_KEYS reads
the supported ids (0x10f), and its scripted presses take it past the title. Two controllers were not run here;
the PR's author played Mario Kart 8 Deluxe in 2-player split screen with an Xbox Wireless Controller and a
DUALSHOCK 4, each its own kart, and a controller disconnected and reconnected during play got its slot back.

Co-authored-by: v0lumehi <34629096+v0lumehi@users.noreply.github.com>
2026-10-24 01:40:58 +08:00
..
patch_nso.py split into the general rewriter and per-build patch files (GitHub issue #63): every game's bring-up edited the one file, so PRs for unrelated games conflicted. dm/tools/patch_nso.py (1,711 → about 1,100 lines) keeps the general passes and the driver; the instruction encoders move unchanged to dm/tools/nso_asm.py; a game's own patches (CUTS, OBS and the frame-step stubs, now FRAME_STEP = {build ID: stub(ctx)}) move verbatim with their comments to dm/games/<id>/patches.py (16 profiles: Tears of the Kingdom, Breath of the Wild, Age of Calamity and Paper Mario: The Thousand-Year Door with their stubs), and the cuts of Nintendo's sdk module builds, 6 of the 15 shipped by 2 to 4 games, to dm/tools/sdk_patches.py. The rewriter loads every patch file, whatever the game, and applies those of a module's build ID at the points it did before (cuts before the scan, gates after the rewrite, stubs after the gates); a build ID claimed twice stops it, and its command line needs no DM_GAME. build_src.sha256 lists the rewriter's files and every patch file (the same for every module). NSP script: the rewrite step's reuse key counts nso_asm.py and only the patch files that name one of the game's builds (patch_files), profile_files leaves patches.py out (only the rewrite step reads it), and a derived profile gets none. dm/tools/tests/test_patches.py: no build claimed twice, the entries' forms, a game's file names only its own profile's non-sdk builds, sdk_patches.py only sdk builds some profile ships, and the reuse key's patch files. Checked byte-identical: every extracted module on the game drive (131 builds) rewritten with the previous and the new rewriter gives the same outputs but build_src.sha256, 28 of the 30 patched builds among them (all four frame-step mains), and 7 fail alike (rtld, and New Super Mario Bros. U Deluxe's, which no profile rewrites); the other two (Super Mario Bros. Wonder's mains and Absolum's, no game files here) by their CUTS / OBS data, equal to the previous tables. AGENTS.md, docs, dm/games/README.md, the profiles' comments and the host comments that named a game's cut in patch_nso.py now name its patches.py or sdk_patches.py
2026-10-23 23:36:21 +08:00

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 through game_profile.h (host) or dm/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.py refuses 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

  1. Run scripts/build_ipa_from_nsp.py on 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, export DM_GAMES_DIR=<NSP folder>/<title ID>_build/profile until the profile moves into dm/games/.

  2. 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.

  3. Add a game profile dm/games/<title ID>/game.tsv, named after the base game's title ID (start from 010015100B514000/game.tsv: identity, build rows, modules; keep the memory / NVN values until they are measured) and build the host for it with make -C dm/host GAME=<id>.

  4. Run the rewriter and check its counts against insn_census.py.

  5. Boot on the Mac and iterate on the failure catalogue in 03 until the game reaches its frame loop.

  6. Extract and translate the shaders, find the binding layout by census, and get the first frame — 04.

  7. Check memory over a minute of running (footprint, the host's memory report line); fix growth before moving on.

  8. Input, audio, then the iOS bundle (d2r-mobile's dm/ios, RomFS inside the app).

  9. Test on a phone: an unsigned IPA the user sideloads, switches through env.txt, results through Documents/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).

  10. 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).