Files
nxnative-easy/dm/games/README.md
T
seedds 79d82f7b64 Remove the D2R code and the legacy frame step
D2R (Diablo II Resurrected) has no profile and its code could not run here: the
host-call svcs its cuts emitted only died, the touch-run-v4 rule set needed a
missing dm/PRODUCT, and the host files its hooks called were never in this
repository. Removed with it:
- rewriter: D2R cuts, observation gates and parameter slots, the touch-run-v4
  rule set and private gates, the boot list, the cut scope column and the
  KRES / KASPECT key slots (fixups.tsv keeps the four thread slots; no
  params.tsv any more); dm/tools/product.sh
- host: D2R save edits, add-on content, render-scale controller, time()
  takeover and Terror Zone clock, crash decoding at D2R offsets, host-call
  registry and stubs, i18n, UI-screen, shell-app loader, zero-stripped
  archives, boot profiling hooks with no implementation; D2R headers, tests
  and scripts; dm/ios/Sources/dm_touch_controls; dm/third_party/libdeflate
- graphics: runtime extrapolation, world freeze, iPad UI shift, shop drag
  image, brightness LUT, visual ruler, bloom / FXAA shader hashes, DM_GFXUSE,
  the eco-fps setting, the loading-screen gate; shader_map.py,
  nometal_run.py, nvn_slots.py, nvn_slots.tsv and the PC-texture samples

The legacy frame step (frame_step legacy, DM_FRAME_STEP) is retired: the
Breath of the Wild and Tears of the Kingdom stubs lose their mode test.
Breath of the Wild's interval-controller cuts stay: the framework keeps the
controller's interval while the game's VFR override holds.

patch_nso.py output is byte-identical for all 15 games on this machine except
the two frame-step stubs; all 18 profiles build; the host tests pass (their
Makefile now builds the general tests only).
2026-10-21 02:09:49 +08:00

24 KiB
Raw Blame History

Game profiles

Each game the port knows has a folder dm/games/<id>/, named after the base game's title ID: Mario's is 010015100B514000, Engage's 0100A6301214E000. The folder holds:

  • game.tsv: the game's constants, one key<TAB>value row each, with comments on their own lines.
  • The shader tables that shader_split.py writes: shader_map.tsv (+ .conflicts.tsv), shader_facts.tsv, shader_pairs.tsv.
  • shader_facts_manual.tsv (optional): hand-written translation facts in shader_facts.tsv's row format, for what the machine code cannot tell (Link's Awakening: a vertex input the integer-input guess gets wrong; Breath of the Wild and Tears of the Kingdom: integer fragment outputs, which a game without a PC version has no other source for). shader_split.py merges it over the generated rows; since the table is on the chain, changing it changes only that profile's chain version.
  • c1_full.tsv: regenerated by c1_guard.py after each shader build.
  • pso_manifest.tsv: the prewarm seed list.
  • vk_capture.tsv (Vulkan games with shader_vk_capture): the keys of the SPIR-V modules the game generates at run time.
  • bf_want.tsv.
  • dmsh_chain.h: the chain version, written by dmsh_chain.py --header.

Everything game-specific belongs here. Code reads the profile; it does not hard-code a game.

Who Picks the profile with Reads
Shader tools (shader_split.py, dmsh_chain.py, dmsh_build.py, dmsh_facts.py) DM_GAME=<id> tables, nvn_*, shader_* keys
Host (make -C dm/host, dm/gfx/tests) GAME=<id> gen/<id>/game_profile.h, generated from game.tsv by game.py header
dm/ios/tools/build_ipa.sh GAME=<id> app name, bundle ID, IPA name, modules, RomFS marker, tables
scripts/build_ipa_from_nsp.py the NSPs' title ID everything above. With no matching profile it writes a survey, then a provisional profile outside the repository and a best-effort IPA (--survey-only stops after the survey)

None of them has a default game: without one they stop and list the profiles. To query a profile:

  • python3 dm/games/game.py list lists the profiles.
  • python3 dm/games/game.py <id> <key> prints one key.

Keys

Key Meaning
title_id, program_id Identity. program_id is what svcGetInfo(ProgramId) reports. The game's name is not stored: the NSP script reads it from the NSP's control data (NACP)
name The game's name, only a label for game.py list, error messages and the Mac window title. The iOS app's name is the game name in the base NSP's control data and its bundle ID is org.nxnative.<title ID in lowercase>; the NSP script's APP_NAME / BUNDLE_ID and build_ipa.sh's DM_DISPLAY_NAME / DM_BUNDLE_ID override them
ipa, app_category iOS app: the default IPA file name and the App Store category
build <ver> <display version> <module>=<build ID>… One row per known version. The NSP script rejects other builds. The host reports the display version by main's build ID
package Version packed into the IPA by default
shader_versions Versions whose union the shader tables are (base game first). All of them are needed to build
modules Code modules rewritten and loaded, in load order. rtld is replaced by the host
romfs_marker A path that must exist in the RomFS
zip_store Extensions the IPA stores without compressing
graphics nvn (the default) or vulkan: the game renders with Vulkan and the host implements its vk* imports on Metal (dm/gfx/vk/hle_vk.m, Hades). Its shaders are the game's SPIR-V files, translated by dm/gfx/tools/vk_shaders.py <RomFS> <shader dir> [--ios <dir>] into <dir>/vk/; build_ipa.sh then ships $MSL_IOS/vk instead of the NVN chain libraries, and the NVN shader tables stay empty
mem_reserve, mem_alias, mem_stack, app_pool Guest address-space partition and application pool (hle_svc.c)
mem_used Memory GetInfo(UsedMemorySize) reports beside the heap (default 0x4000000, 64 MB). SetHeapSize checks a request against the real size of the loaded modules, so a game that sizes its heap as total − used and has more than 64 MB of modules needs at least their size here. Hyrule Warriors: Age of Calamity: 66.6 MB of modules, so the default made SetHeapSize(pool − 64 MB) fail and nnMain return at once; it sets 72 MB
game_args Arguments the game gets when the command line gives none (the iOS app, the NSP script's Mac runs), split at spaces; dmhost <dir> -- … overrides them. The SDK hands them to the game before nninitStartup, as on hardware (main.c). Octopath Traveler 0 caps the address region Unreal's nninitStartup reserves (62 GB by default) with -SwitchVirtualAddressSpaceSize=2048 (MB)
present_interval game (the default): frames are paced by the present interval the game sets, as on the Switch (a frame presented with interval 2 stays on screen for two display ticks; a game that keeps asking for 2 for a second gets the 30 fps target, and on iOS the display link then runs at 30 Hz; Mario 1.2.1 switches to 2 for single frames after a slow one; Engage asks for 2 on the Switch, and a rewriter cut on its 2.0.0 main makes it ask for 1, see 0100A6301214E000/game.tsv). display: one frame per display tick whatever the game asks, which is only right for games whose logic does not count frames (D2R's runs on its own 25 Hz clock). DM_PRESENT_INTERVAL=game / display overrides it, also from Documents/env.txt
fps_target Frame rate cap of the rewriter's frame step, for builds that have one (patch_nso.py FRAME_STEP: Tears of the Kingdom 1.4.3, Breath of the Wild 1.9.0), or the engine's own frame limit the host writes into game memory (hle_fpslock.c: Monster Hunter Rise 16.0.2, which also needs present_interval display). 0 (the default): the game's own step. 30 / 60: the host decides each frame's step in whole vsyncs and keeps the frame on screen that long (hle_nvn.c fstep_tick, nvn_fstep.h): 1 at a 60 cap, 2 when 60 does not hold or at a 30 cap (and while a prerendered movie plays), plus one vsync while the game is behind real time after a late frame. Presents follow that hold instead of the interval the game sets. DM_FPS_TARGET overrides it, also from Documents/env.txt; the log line frame step: game time … of real time checks the game's speed
core_preempt on / off (the default): approximate Horizon's per-core preemption. When an SVC wakes a higher-priority thread pinned to the caller's core, the caller waits (at most 5 ms) until such threads block again (hle_thread.c). Metroid Dread's movie player relies on it; Mario's loading gets slower with it (first frame 0.6 → 2.9 s), so it is off unless a game needs it. DM_PREEMPT=0 / 1 overrides
alloc_big_align on / off (the default): nn::mem::StandardAllocator::Allocate aligns blocks of 64 KB or more to 64 KB (hle_os.c). Vanillaware's engine (13 Sentinels: Aegis Rim, Unicorn Overlord) takes a memory pool's storage from malloc and cuts ring heaps out of it at 64 KB alignment relative to the pool's GPU address. On the Switch that address is big-page aligned; the host's is the CPU address, only 4 KB aligned, so the last ring heap does not fit and keeps base 0, and the game writes through a null pointer (13 Sentinels entering its first battle, Unicorn Overlord past the title screen)
shared_font What the shared-font address points at: bfttf (the default) = the 8-byte BFTTF header (with the "already decoded" marker) followed by the TTF, which Mario passes to ScalableFontEngineHelper::Decode; ttf = the TTF itself, which Super Mario 3D World + Bowser's Fury (SDK 10) hands straight to ScalableFontEngine::LoadFont (with the header the engine fails with error 403 and the game crashes looking up a glyph)
device_api NVN API version major.minor that nvnDeviceGetInteger reports (default 55.15, what Mario requires). Games check it and skip creating their device on a mismatch: Super Mario 3D World + Bowser's Fury (SDK 10) requires major 53 and minor ≥ 313. DM_NVN_API overrides. Not part of the shader chain version (only nvn_* / shader_* keys are)
op_mode The operation mode the game sees at start: console (the default, docked) or handheld. Super Mario Galaxy uses handheld: docked, its pointer is the Joy-Con gyro, which the host does not have; handheld, it is the touch screen, which the iPhone front end feeds with the touches no gamepad control takes (DmGamepad.m). Handheld also renders at 720p. DM_OPMODE=0 / 1 overrides, also from Documents/env.txt
perf_mode follow (the default): the performance mode follows the operation mode (Boost docked, Normal handheld); boost: Boost whatever the operation mode (hle_oe.c), a combination a Switch 1 never reports, so a profile choice. Rhythm Heaven Groove plays handheld with Boost: docked means a TV to the game, which then asks for a latency calibration, and only Boost gives its 1920x1080 draw size (handheld Normal draws 1280x720)
pad_layout The iPhone virtual gamepad's layout on first launch: simple (the default) = 2D (left stick, A/B/X/Y, shoulders, − / +), full = 3D (adds a D-pad, a right stick and L3 / R3). The layout button at the top centre switches between them in game and the app remembers the player's choice (DmGamepad.m)
feedback_snap Fragment shader keys (prefixes) whose draws sample a colour attachment of their own render pass and must read a snapshot instead (fb_prepare in nvn_mtl.m). Writing and sampling one attachment in a render pass is undefined in Metal: the Mac happens to read the old content, the iPhone sometimes reads whole 16×16 blocks as magenta. Each such draw costs a pass split and a copy, so only the shaders seen to break are listed (Super Mario 3D World + Bowser's Fury: its final composite). DM_NVN_FB_SNAP=0 / all / <keys> overrides, also from Documents/env.txt
alpha_from_red Fragment shader keys (prefixes, up to 48) that read a single-channel render target whose channel mapping is exactly (R, 0, 0, 1) as (R, 0, 0, R) (tex_alpha_red in nvn_mtl.m). It was a workaround for NintendoWare layout captures: the game rebuilds each capture with its own mapping ((1, 1, 1, R) for one tagged A8), and the textures kept (R, 0, 0, 1) only while the host's nvnTextureCompare ignored the mapping (fixed, see 04). Breath of the Wild no longer lists it; Super Mario Odyssey still lists its 26 layout shaders. DM_NVN_ALPHA_RED=0 / <keys> overrides, also from Documents/env.txt
tex_flush_upload on / off (the default): a texture the game fills from the CPU in place (no packaged layout, no WriteTexels) and publishes with nvnMemoryPoolFlushMappedRange is uploaded from guest memory like a packaged one, and re-uploaded when its pages change (ft_add in hle_nvn.c). Tears of the Kingdom streams all its textures this way; without it the 3D scene is black. Off by default because a flushed range in another game could cover an image a shader writes, whose GPU content the upload would replace
render_cond on / off (the default): SetRenderEnableConditional / SetRenderEnable are recorded and evaluated at replay, and a timestamp report's counter field is 0 as on the GPU (OP_RENDER_COND in nvn_mtl.m). Tears of the Kingdom wraps its clears in conditions that skip clearing a target nothing has drawn into since its last clear; without the key every clear runs and its 3D scene is black. Off by default: other games draw everything, as before
tex_dirty_upload on / off (the default), needs tex_flush_upload: a registered texture whose pages the CPU writes after nvnTextureInitialize without any NVN flush is uploaded as well (its pages are write-protected at registration, checked on first use; nvn_ft_written in hle_nvn.c). Tears of the Kingdom fills its ASTC terrain material arrays this way; without it the cave is magenta
tex_dyn_slots on / off (the default): a shader whose texture handle the translator could not trace to one slot gets Ryujinx's fallback arrays (`_<t
gpu_timestamps What a timestamp report (nvnCommandBufferReportCounter) records: submission (the default) = the time the whole submission completed, the same for every report in it; segment = the GPU start (TIMESTAMP_TOP) or end (the other types) of the segment the report was encoded in (the host commits a submission in segments of 64 draws, sub_cut in nvn_mtl.m), so two reports of one submission bracket the GPU time between them, as on hardware. Breath of the Wild needs segment: its frame-interval controller and its dynamic resolution read the GPU time between the frame's first and last report, and with one time for both they read 0 (the game kept 60 fps in slow motion when the GPU could not keep up). DM_NVN_GPU_TS=segment / submission overrides it, also from Documents/env.txt
vconv_range on / off (the default): vertex attributes the replay converts on the CPU (integer shader inputs fed float-type formats and the like, vconv_bind in nvn_mtl.m) are converted only over the vertices each draw reads (first / count, or the smallest and largest index of a direct indexed draw, kept while the index pages stay unwritten) instead of the whole bound range. Age of Calamity binds whole mesh pools: in a battle that was millions of vertices a frame. Indirect draws, per-instance streams and indices with a GPU write queued on them keep the whole range. DM_NVN_VCONV_CHECK=1 scans the indices again at every reuse and reports differences; DM_NVN_VCONV_RANGE=0 / 1 overrides it
nvn_tex_base, nvn_tex_slots, nvn_samp_base, nvn_img_base, nvn_img_slots, nvn_ssbo_bases NVN driver constant segments, measured over all shaders
nvn_septex_base, nvn_septex_slots Optional: a separate texture handle table of its own, for a game that binds both kinds of handle. On hardware nvnCommandBufferBindTexture and nvnCommandBufferBindSeparateTexture(s) fill two tables of the driver segment; without these keys the host keeps both kinds in one (Mario binds only combined handles, Engage only separate ones). Monster Hunter Rise binds both, slot 0 of each in the same frames: combined handles at nvn_tex_base 0x20, separate textures at 0x168. With the keys, separate binds go to slots 128 + n of the replay's table (SEPTEX_SLOT0 in hle_nvn.c), dmsh_build.py names that table's handles 128 + n, and the replay fills both tables of c2
shader_groups, shader_note shader_split.py source groups and the map header note (Engage: engine for data.unity3d and Resources/, assets for the Addressables bundles)
shader_main_glslc on / off (the default): shader_split.py also reads GLSLC compiler outputs (magic 0x19866891) in main. 13 Sentinels keeps all its shaders there; Engage's main has 11 that it does not use, so the key stays off for other games
shader_separable on / off (the default): the game builds single-stage programs and combines them per draw, so the real vertex / pixel pairs exist only at run time. shader_split.py then pairs every vertex shader with a cover of pixel shaders that together read every input any pixel shader reads, and dmsh_build.py's vs_pad makes every vertex shader link with any pixel shader (13 Sentinels)
shader_vk_capture on / off (the default), with graphics vulkan: the game generates its SPIR-V shaders at run time (Super Mario Galaxy: Hagi's GX shaders, 3,089 modules created from its pipeline database while it shows "Initializing system..."), so no game file holds them. They are captured on the Mac with DM_VK_CAPTURE=<dir> (dm/gfx/vk/hle_vk.m; the NSP script does it itself, booting the game in the Mac host until every key the profile's vk_capture.tsv lists is there), and vk_shaders.py translates the capture folder with the RomFS
shader_glslc_capture on / off (the default): the game compiles some shaders at run time with its own GLSLC (Fire Emblem: Three Houses: 9 built-in KTGL shaders at boot), so no game file holds them. They are captured on the Mac with DM_GLSLC_CAPTURE=<dir> (dm/host/hle_glslc.c; the NSP script does this itself, booting the game in the Mac host until every capture file the profile's shader_map.tsv names is there), and shader_split.py reads <version dir>/glslc_capture/*.glslc (beside that version's RomFS) or $DM_GLSLC_CAPTURE; without the folder those shaders get no translation and the tables differ from the profile's
shader_ue_decode on / off (the default): an Unreal Engine game keeps its shaders in the shader code archives of its paks (*.ushaderbytecode), each shader Oodle-compressed, which only the game's own Oodle decodes. The NSP script (decode_ue_shaders) takes the archives out of the paks with dm/gfx/tools/uepak.py (the pak index is AES-encrypted with a key it finds in main), boots the game in the Mac host with DM_OODLE_JOB (dm/host/hle_oodle.c: the game's OodleLZ_Decompress decodes every shader right before nnMain, then the host exits) and writes the decoded archives to <version dir>/ue_shaders, where shader_split.py reads them (DM_UE_SHADERS overrides). Octopath Traveler 0: 10,596 shaders in 2 s. Its vertex and pixel shaders combine per draw, so the profile also sets shader_separable

The translator (dmsh_xlate/Program.cs) still has Mario's texture / sampler bases and the depth convention built in. It uses the bases only to look up per-slot texture / image types in shader_facts.tsv; dmsh_build.py decodes the bindings from the profile. So a profile with other bases (Engage: 0x168 / 0x568) builds as long as its facts table has no per-slot texture / image entries; otherwise dmsh_build.py stops, and the translator must take the bases from the profile first.

Adding a game

  1. Survey. Run scripts/build_ipa_from_nsp.py on the game's NSP folder. Without a profile it extracts only the code modules and a RomFS list, then writes <NSP folder>/<title ID>_survey/survey.md. The survey covers modules and build IDs, SDK, engine and graphics API markers, RomFS containers, the rewrite-site census, and imports compared with the host hooks. Unless told --survey-only, the script then writes a provisional profile to <NSP folder>/<title ID>_build/profile/<title ID>/ (outside the repository) and carries on to a best-effort IPA. dm/games/profile_guess.py fills its game.tsv from the game files, each key under the reason for it (the survey lists them too):

    • identity and name, the build row it found, the largest RomFS file as romfs_marker, big file types in zip_store, shader_groups by RomFS top-level folder, Mario's NVN values (Engage's texture base under Unity IL2CPP);
    • graphics vulkan when main imports Vulkan functions and not nvnBootstrapLoader, then shader_vk_capture on when the RomFS has no SPIR-V file;
    • modules: a subsdk is loaded only when main imports something from it that the host does not supply itself. Under the Vulkan layer NVIDIA's OpenGL / Vulkan drivers, the Vulkan loader and GLSLC are dropped; movie modules and a GLSLC that main calls stay;
    • shader_main_glslc and shader_separable when main holds GLSLC outputs and the RomFS has no BNSH / BAHS / UnityFS; shader_glslc_capture when main also holds GLSL sources and calls glslcCompile in a module the game ships;
    • the memory partition. Without a system resource in the NPDM: the 4 GB layout, its heap region the pool − 64 MB − the bss of giant modules (Metroid Dread: 0x90000000). When main never sets a heap: a 1 GB block (Super Mario Galaxy). With a system resource and a heap: Engage's 5 GB block with the hardware pool, which the probe corrects;
    • SDK 8 / 10 NVN games: shared_font ttf and device_api 53.313. Engine presets: Hagi (hagi.config) gets op_mode handheld and pad_layout full.

    python3 dm/games/profile_guess.py --check runs these rules on every profile in dm/games (with the extractions under /Volumes/T7/nsp) and lists the keys where the guess differs.

    After the rewrite, mem_used is set when the rewritten modules exceed the default 64 MB (a game without a system resource). A game with a system resource that sets a heap then gets a heap check: it boots in the Mac host (headless, no shader libraries) until its first heap request, or 5 s into nnMain without one, at most 30 s (HEAP_CHECK_SECS). A request the heap region cannot hold (SetHeapSize OOM) sets the alias / heap split (Tears of the Kingdom asks for 0x86000000, which gives its hand-made 0x98000000 alias region; Wonder's 1.44 GB gives 0xc0000000), and a pool check that fails for good sets mem_used. The profile gets a # heap check: line, and later runs keep its keys (delete the line to check again; --no-heap-check / HEAP_CHECK = False skips it). Other games skip the check, so it costs nothing for them and a few seconds plus a Mac host build for the rest.

    No short run decides render_cond, tex_flush_upload, tex_dirty_upload, core_preempt, gpu_timestamps, feedback_snap, alpha_from_red, vconv_range or present_interval, nor op_mode / pad_layout outside the presets: they keep their defaults for a developer. A minute's boot without input counted Wonder's clears under render conditions (65,536 or more; it needs no render_cond) as readily as Tears of the Kingdom's, saw Breath of the Wild flush 32 CPU-filled textures with data (it needs no tex_flush_upload) and Age of Calamity none (its movie frames come later), and Metroid Dread's movie crash shows in about half the runs. On the iPhone, Documents/env.txt can still set DM_OPMODE, DM_NVN_GPU_TS and DM_NVN_FB_SNAP, and the pad layout switches in game.

    The shader tables, dmsh_chain.h and c1_full.tsv are written into the same folder. A second run reuses that game.tsv (delete it to survey again), so it can be edited by hand. --provisional builds from a provisional profile even for a game that has one in dm/games, to test the guesses: DM_GAMES_FIRST=1 makes every tool search DM_GAMES_DIR first, and the IPA gets -provisional in its name. Tools find profiles outside dm/games through DM_GAMES_DIR (folders separated by :, searched after dm/games); make -C dm/host reaches such a profile through the symlink dm/host/gen/<id>/profile. To keep a game, move the folder into dm/games/ and bring it up as below.

  2. Profile. Copy 010015100B514000/game.tsv (or the provisional profile) to dm/games/<title ID>/game.tsv (the base game's title ID, as the folder name) and fill in the identity, build rows and modules. Leave the memory and NVN values as Mario's until the bring-up measures them (docs/agents).

  3. Shader tables. Run DM_GAME=<id> shader_split.py … dm/games/<id>/shader_map.tsv dm/games/<id>/shader_facts.tsv dm/games/<id>/shader_pairs.tsv. Add an empty shader_map.tsv.conflicts.tsv, bf_want.tsv and pso_manifest.tsv (copy Mario's headers).

  4. Chain.

    1. DM_GAME=<id> dmsh_chain.py --header
    2. DM_GAME=<id> [IOSOUT=…] dmsh_build.py <pkg> <cache>
    3. c1_guard.py <cache>/b-<V> dm/games/<id>/c1_full.tsv
  5. Host bring-up. Run make -C dm/host GAME=<id>, then work through the bring-up loop in docs/agents. Code that only fits one game is gated by its build ID (dm_main_build()), never left to run on every game.

  6. IPA. Use GAME=<id> dm/ios/tools/build_ipa.sh, or the NSP script, which now finds the profile.