Files
nxnative-easy/docs/agents/02-survey.md
T
seedds 4d8ac53681 Into the opening course: resource heap slack, compute start offset, CPU-written text
- 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
2026-10-13 08:13:48 +08:00

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::audio 197, nn::hid 48). 172 of the host's 186 hook names existed in Mario's sdk.
  • 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 reach nn::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 in nn::sm::Initialize with 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*, x18 use, 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 reads CTR_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 logs SVC %#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) calls GetProcAddress per 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 (Mario main, 28 MB: ~35 min, 138k functions). Ghidra's image base for these ELFs is 0x100000, so FUN_xxxx = module offset + 0x100000.

  • Decompile on demand in seconds with a script (ghidra/scripts/Decompile.java in 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,0x3707b0
    

    analyzeHeadless needs JAVA_HOME set even when Java is on PATH. Code only reachable through vtables may have no function defined at an address; use objdump -d --start-address=… --stop-address=… <module>.elf instead.

  • 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_HOME and a long REA_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::Decode in sdk.