- patch_nso.py: Mario main cut gives every resource heap +16 KB; Model/NpcCommonerA.bfres came up 0x70 bytes short of its size-table budget while loading Course900 - dmsh_build.py: compute instructions start at code area +0x100, not +0x30; the misaligned decode turned 12 particle / post-effect compute shaders into endless loops (GPU hang) - Linear textures in CPU-visible pools are built on the pool buffer, so the CPU-rasterized glyph cache reaches the GPU (menu and dialogue text) - Host command-memory segments grow on demand; RGB10A2SN vertex format; instanced DrawArrays - DM_PROBE: thread IDs, register-relative dumps, hit filters, ExpHeap walker - dm/tools/ghidra/Decompile.java; agent guides and README updated
5.3 KiB
02 — Surveying a new title
Do this before touching the host. Every item answers "what will break, and how much work is it".
Identity and versions
| Question | How |
|---|---|
| Program ID | NCA header / main.npdm. The host reports it from svcGetInfo(ProgramId) (hle_svc.c, DM_PROGRAM_ID overrides); some SDK code checks it. |
| SDK version | Strings in sdk: SDK MW+Nintendo+NintendoSDK_nnSdk-16_2_1-Release style. SDK 16 changed several object sizes and service paths compared with D2R's SDK. |
| NVN API version | The game calls nvnDeviceGetInteger(API_MAJOR/MINOR) and refuses to run if the host reports an older version. Mario required 55.x with minor ≥ 15 (DM_NVN_API). Its device setup returns silently on a failed check, so a missing frame loop with no error is a hint. |
| Modules | ExeFS listing. Only main + sdk so far; subsdk* would need loader support. |
Imports vs. host hooks
dm/tools/nso_syms.py <repo dm/host dir> <gameA ExeFS> <gameB ExeFS> compares the dynamic imports of the new title (B) with the title the host already supports (A) and with the host's hook names.
- Mario imported 1,107 symbols, 692 of which D2R never imported (biggest gaps:
nn::audio197,nn::hid48). 172 of the host's 186 hook names existed in Mario'ssdk. - Not every import needs a hook. Many SDK functions run natively inside
sdk; only the ones that end in an IPC to a system service (they reachnn::sm::Initialize/svcSendSyncRequest) must be replaced by a host hook on the public entry point. A missing one shows up at run time as an abort innn::sm::Initializewith a backtrace: hook the outermost public function in that backtrace.
SVCs and instructions to rewrite
dm/tools/insn_census.py <module.nso> [--sites tsv]counts every site the rewriter must handle:svc,mrs/msr tpidr*,x18use, exclusive windows (ldxr/stxr), timers (cntpct,cntfrq), cache maintenance, and system registers.- Run
DM_RULESET=native python3 dm/tools/patch_nso.py <nso> <out dir> <name>and check that its counts match the census. New system registers appear with new SDKs: SDK 16 readsCTR_EL0(macOS traps it), now rewritten to a constant. - Mario's SVC set was a subset of D2R's. A new SVC needs a case in
hle_svc.c/hle_thread.c; the default case logsSVC %#x (占位返回成功)once and returns success, which is wrong for anything that returns data.
NVN surface
- Find how the game obtains NVN entry points. D2R wrote slots with
adrp-to-slot code (dm/gfx/tools/nvn_slots.py); Mario's loader (main+0x1070910) callsGetProcAddressper name and stores through the GOT (dm/tools/nvn_slots_got.py <main.bin> <loader> <text end> <out tsv>). The output lists every fetched function and every call site. - Mario fetched 534 functions and called 236 of them; 79 were new to the host (CommandBuffer 31, Texture 15, Buffer 10, Sampler 10). Names the host doesn't know at all go into
dm/gfx/nvn/names.txt→gen_protos.py.
Data containers that matter to the host
| What | Where to look | Mario |
|---|---|---|
| Shaders | Every container that embeds NVN shader binaries, not just the obvious archive. Scan all of RomFS and main (see 04) |
Shader/*.bfsha (BNSH), Lib/agl/*.sarc + Shader/Secred.sarc (agl BAHS sharcb), Effect/*.esetb (BNSH), Layout/*.blarc (BNSH), and two BNSH inside main |
| Heap budget | A heap-definition file, if the engine has one | System/HeapSize/*.HeapDef.byml: per-subsystem heaps in KB, and a 1.75 GB VirtualAddressMemory mapped on demand into the alias region (drives the address-space partition, 03) |
| Resource sizes | Resource size table | System/Resource/ResourceSizeTable.*.rsizetable (RESTBL, CRC32 of the path → decompressed size) |
| Fonts | Font archives and use of the system shared font | Font/*.bfarc; the shared font goes through nn::fontll::ScalableFontEngineHelper::Decode (BFTTF) |
| Languages | Per-language archives | Mals/<Region><lang>.sarc, Voice/Resource/<Region><lang> |
Ghidra and REA
-
Convert with
nso2elf.py, create one persistent headless project per module, and analyse once (Mariomain, 28 MB: ~35 min, 138k functions). Ghidra's image base for these ELFs is 0x100000, soFUN_xxxx= module offset + 0x100000. -
Decompile on demand in seconds with a script (
ghidra/scripts/Decompile.javain the workspace):JAVA_HOME=<jdk21> $GHIDRA/support/analyzeHeadless ~/<title>-work/ghidra/proj <name> -process <module>.elf \ -noanalysis -readOnly -scriptPath <repo>/dm/tools/ghidra -postScript Decompile.java 0x107dbc0,0x3707b0analyzeHeadlessneedsJAVA_HOMEset even when Java is on PATH. Code only reachable through vtables may have no function defined at an address; useobjdump -d --start-address=… --stop-address=… <module>.elfinstead. -
REA (MCP) can open the same ELF, but its snapshots keep evidence only, so every new session re-runs the 35-minute analysis; the persistent Ghidra project is faster for repeated lookups. Register REA with
GHIDRA_INSTALL_DIR,JAVA_HOMEand a longREA_GHIDRA_STARTUP_TIMEOUT_MS. -
Dynamic symbols (imports / exports with names) come from the NSO's
.dynsym; read them directly (MOD0 → dynamic section →DT_SYMTAB/DT_STRTAB) when you need an address for a named SDK function, e.g.nn::fontll::ScalableFontEngineHelper::Decodeinsdk.