Files
nxnative-easy/dm/games/README.md
T
seedds 6d42bc9815 Advance Wars 1+2: Re-Boot Camp (0100300012F2A000, base game 1.0.0, Unity 2020.2.1f3 IL2CPP on SDK 10.4.1): game profile, the SDK 10.4.1 sdk build's address-region cuts and a shader split reader for control sections loose in main. Plays on the iPhone (IPA 7.1 GB, user). On the Mac from the boot through the Bink opening movie, the title, Nell's introduction, a new casual campaign and the first training battle (Field Training, Troop Orders): 60 fps in the menus, 30 fps in battle by the game's own limit (present interval 1, the frame's work 33 ms with CPU and GPU mostly idle), FMOD sound through AudioOut, about 2.5 GB resident and 1.6-1.9 GB of Metal allocations, level over a 15-minute run with no red line. sdk_patches.py: the sdk build is The Great Ace Attorney Chronicles' (no virtual address memory there, so it never needed a cut); this game's NPDM declares 16 MB of system resource and nninitStartup (main+0x365310) hands pool - used to StandardAllocator, whose Initialize (sdk+0x2edf90) aborted at boot. The AddressRegionManager constructor (sdk+0x340e90) loads the 1 GB node area with orr w8 / w9, wzr, #0x3fe00000 (the SDK 7 / 8 form, +0x340ea0 / +0x340f3c), cut to 62 MB, and AllocateAddressRegion's 1 GB alignment of regions of 1 GB and more (+0x34109c) to 32 MB, as on DRAGON QUEST XI S's SDK 7.3.2 build. Profile: the NSP script's heap check set a 1 GB pool, as for the other Unity games with virtual address memory, but the game's malloc then ran dry at the first loading screen and Unity's GPU pool allocation (main+0x96f430) halved its aligned_alloc request down to 0 and spun there (frames stopped 17 s in; found with sample on the Mac host); with 2 GB the alias region is mapped up to 2.26 GB (high-water mark +3.27 GB of 4.375) in Engage's 5 GB block. shader_split.py raw_shaders: Bink 2's Unity plugin builds its programs from 32 control sections and code areas in main's read-only segment that are in no GLSLC output and no BNSH file (2 vertex, 22 pixel, 8 compute), so the movies drew white (two shaders not in the map and a program without a vertex shader as the opening movie started). 26 are 0x20-byte records in .data ({code pointer, control pointer, u32 code length, u32 control length}, found through the relative relocations, RELA or RELR: rel_targets); the code takes the other 6 by address, their code area right after the control section's own length or right before it, and two share one; both neighbours can pass the header and length checks (main+0x4a2c670, a compute shader, would have taken the first 0x100 bytes of the next vertex shader's code), so only a code area whose address the code builds with adrp + add nearest to the control section's is taken (adrp_targets). Compute code areas have no header (SDK 10), so the stage is glslc_stage's, and a code area may not hold a control section. Control sections another reader took from main are left alone (Celeste's MonoGame effect). Of the 25 mains on the game drive the reader adds shaders only to this one, so every other profile's tables stay as they were (Celeste and the other games on the second drive not checked: not mounted). 20,424 shaders, all build for the Mac and iOS. Checked: the NSP script packs it from the NSP alone (Mac and iOS), the tool tests pass. README (games table, a Mac picture of the first battle), AGENTS.md (52 -> 53), dm/games/README.md (the heap check's 1 GB is a starting point), docs/development.md and docs/agents 03 / 04 say so
2026-10-24 17:13:12 +08:00

34 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.
  • pso_manifest.tsv: the prewarm seed list.
  • vk_capture.tsv (a Vulkan game that generates its shaders at run time, Super Mario Galaxy): the keys of the SPIR-V modules it generates.
  • gl_capture.tsv (an OpenGL game that puts its GLSL together at run time, Balatro): the files (<key>.vert / <key>.frag) of the shaders it compiles.
  • bf_want.tsv.
  • patches.py (optional): the rewriter's patches of the game's own builds (main), keyed by NSO build ID: CUTS (byte cuts, each original asserted), OBS (observation gates) and FRAME_STEP (frame-step stubs). dm/tools/patch_nso.py loads every profile's file and applies those of a module's build, wherever it comes from; a build claimed twice stops the rewrite. Cuts of Nintendo's sdk modules, which several games ship, are in dm/tools/sdk_patches.py. A game without patches has no file, and a derived profile never has one. dm/tools/tests/test_patches.py checks that a file names only its own profile's builds.

The chain version is not in the profile: one version serves every game (dm/gfx/nvn/dmsh_chain.h, written by dmsh_chain.py --header). Two more tables live in the profile folder but are not committed (git ignores them). A full dmsh_build.py run makes both from its Mac outputs, so each checkout makes its own, and stamps its libraries with the digest of the profile's inputs (b-<V>/inputs.tsv, dmsh_chain.py --inputs). The host takes a table only when its header names its library's chain version and that digest.

  • c1_full.tsv: written by c1_guard.py. Without it every shader's c1 gets its own 64 KiB.
  • cb_extent.tsv: how far into each uniform slot every shader can read, written by cb_extent.py. Without it a uniform block the game updates in part inside a render pass is copied whole.

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

Launcher settings. settings.tsv lists the player-facing settings once (the environment variable each sets, its kind, default and the condition under which a game offers it; its header describes the columns). build_ipa.sh runs settings.py <id> <rewritten modules> <control.nacp> <out.json>, which keeps the rows that hold for the game: a profile macro (DM_GAME_FPS_TARGET!=0), a key in main's rewriter table (RENDER_SCALE, written by patch_nso.py for the builds with a render-scale stub), the game's languages. It writes them into the app as dm/launcher.json, from which the iOS launcher (dm/ios/Sources/Launcher.swift) draws its screen. A new player setting is one row; diagnostic switches stay environment-only.

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 version the game was brought up on. The NSP script builds any version (the base game alone, any update): a registered one from the profile as it is, any other from a profile derived from it (<id>_build/profile: the same keys, that version's own shader tables; derive_profile), with a red line naming the modules whose build no row registers, which the per-build cuts and host fixes do not reach. The host reports the display version by main's build ID
package The version the profile's tables, prewarm list and notes were made on. The NSP script packs the newest version in the folder (-p / PACKAGE picks another)
shader_versions Versions whose union the shader tables are (base game first). A registered version is built from the profile as it is only when all of them are in the folder; otherwise from a derived profile, with tables from the packed version alone
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; empty checks the directory itself for a program without a data section (ANIMAL WELL)
zip_store Extensions the IPA stores without compressing
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. Cuphead: 80 MB of modules, 84 MB
sys_resource The system resource size the NPDM declares (main.npdm META +0x14), which GetInfo(TotalSystemResourceSize) reports (hle_svc.c). The SDK's IsVirtualAddressMemoryEnabled is "that size is not 0", and some games choose their memory setup by it: Big Brain Academy: Brain vs. Brain's nninitStartup asks StandardAllocator for a 60 GB address region with virtual address memory and sets a heap without it, and its NPDM declares none (0). The survey writes it from the NPDM for every new game; a profile without it gets the host's earlier answer, 32 MB, for every game
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)
game_env KEY=VALUE pairs, separated by spaces, that the game's getenv returns (hle_os.c; any other name goes to the SDK's own getenv, whose environment is empty as on Horizon). For runtimes that take their settings from the environment. Absolum (.NET NativeAOT) sets DOTNET_GCRegionRange=60000000: the GC reserves 5 × its heap hard limit as its region range otherwise, 3.75 GB, which only the Switch's 64 GB alias region holds. DM_GAME_ENV overrides it, also from Documents/env.txt
mem_map_limit on / off (the default): MapPhysicalMemory fails with ResultLimitReached when it would take the process past the application pool (the pool less the used memory GetInfo reports), as on Horizon (hle_svc.c). Pokémon Legends: Z-A allocates and frees everything it can at startup and the SDK keeps the freed pages mapped until a map fails, then unmaps them and retries; without the limit they stay mapped and the game maps past the pool. Off by default: Ace Attorney Investigations Collection maps a little more than the 1 GB pool its profile reports
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; Monster Train 2 asks for 2 and steps by Unity's frame time, so it runs at 60 fps: screenshots at the same moments match at 30 and 60 fps). 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 (FRAME_STEP in the profile's patches.py: Tears of the Kingdom 1.4.3, Breath of the Wild 1.9.0, Hyrule Warriors: Age of Calamity 1.3.0, Paper Mario: The Thousand-Year Door 1.0.1), 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; Octopath Traveler 0 1.0.5, Unreal's t.MaxFPS and rhi.SyncInterval, so the game asks for present interval 1 itself; Luigi's Mansion 3 1.4.0, its frame limiter's milliseconds per frame, with 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
fps_hold on (the default): with fps_target, three late frames in a row raise the hold above the cap's vsyncs (30 or 20 fps), and steady frames that fit try the lower hold again. off: frames are never held longer than the cap's vsyncs; each is shown as soon as it is ready. For a game whose updates do not wait for the presents and step by the cap whatever the hold (Age of Calamity): there the hold only paces the screen. DM_FPS_HOLD=on / off overrides it, also from Documents/env.txt Paper Mario: The Thousand-Year Door sets it off too: its frame waits for three render worker threads, and holding frames left them idle, so a frame cost more the longer the hold (the hold stuck at 3 vsyncs, 20 fps, on a loaded Mac; 32–58 fps without holds)
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 Optional, decided by the game files without it (the SDK version the host reads from the sdk module, loader.c dm_sdk_major, and main's imports, dm_main_imports): what the shared-font address points at. bfttf (SDK 11 and later, when main imports ScalableFontEngineHelper::Decode) = the 8-byte BFTTF header (with the "already decoded" marker) followed by the TTF, which Mario passes to Decode; ttf = the TTF itself, which SDK 10 and older games hand straight to ScalableFontEngine::LoadFont (Super Mario 3D World + Bowser's Fury: with the header the engine fails with error 403 and the game crashes looking up a glyph; Odyssey, Link's Awakening, Age of Calamity and Three Houses alike) and Unity's player to FreeType (Patrick's Parabox: with the header its Japanese, Korean and Chinese text drew nothing). No profile sets it
device_api Optional, decided by the SDK version without it: the NVN API version major.minor that nvnDeviceGetInteger reports, 55.15 for SDK 11 and later (what Mario requires) and 53.313 for SDK 10 and older (hle_nvn.c nvn_api). 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. No profile sets it. DM_NVN_API overrides. Not part of the shader chain version (only shader_* keys are)
op_mode The operation mode the game sees at start: console (the default, docked) or handheld. Super Mario Galaxy and Captain Toad: Treasure Tracker use handheld: docked, their 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 (in the 3D layout, outside the right third of the screen, which is the right stick; 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 right stick is not drawn: a touch landing in the right third of the screen on no control holds it, centred where it lands), touch = Tap (no control but the layout button: every touch goes to the game's touch screen; needs op_mode handheld, Balatro). The layout button at the top centre goes to the next layout in game (Tap only in handheld mode) 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 usually 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; Captain Toad: Treasure Tracker: the same agl program, whose blocks showed on the Mac too). 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. Without the key, every game still gets a texture's storage once when the game flushed it before the texture's first use (nvn_flushed in hle_nvn.c: Breath of the Wild's sky tables); the key is for textures the CPU writes again after that
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. Animal Crossing: New Horizons too: four clear-only passes wiped its composite before the window copy, and every frame was 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. FINAL FANTASY TACTICS - The Ivalice Chronicles loads its UI textures straight into CPU-uncached pools, which need no flush, after creating them; without both keys its screen stays black. The Great Ace Attorney Chronicles copies every texture into a CPU-cached pool after creating it and never flushes (its main has no cache maintenance at all): black too
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
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)

What the game files decide at every build

These are not profile keys: the tools read them from the game files each time, by rules checked against every profile (dm/games/profile_guess.py shader_sources).

  • Graphics API. A game whose main imports Vulkan functions and not nvnBootstrapLoader renders with Vulkan, which the host implements on Metal (dm/gfx/vk/hle_vk.m; Hades, Super Mario Galaxy). Its shaders are its SPIR-V files, translated by dm/gfx/tools/vk_shaders.py <RomFS> <shader dir> [--ios <dir>] into <dir>/vk/, and build_ipa.sh ships $MSL_IOS/vk instead of the NVN chain libraries (GFXAPI, which the NSP script passes; by hand the shader libraries there tell). The NVN shader tables stay empty.
  • SPIR-V generated at run time. A Vulkan game without SPIR-V files in its RomFS generates its shaders at run time (Super Mario Galaxy: Hagi's GX shaders, 3,089 modules created from its pipeline database while it shows "Initializing system..."). The NSP script captures them on the Mac with DM_VK_CAPTURE=<dir> (dm/gfx/vk/hle_vk.m), 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. An OpenGL game with no GLSL source in its RomFS or main (Balatro: LÖVE puts its shaders together in Lua) is captured the same way with DM_GL_CAPTURE=<dir> (dm/gfx/gl/hle_gl.m) until every file in gl_capture.tsv is there, and gl_shaders.py translates the capture.
  • Shaders compiled at run time. A game whose main holds GLSL sources and calls glslcCompile in a module it loads 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. The NSP script captures them on the Mac with DM_GLSLC_CAPTURE=<dir> (dm/host/hle_glslc.c), 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 it prints a red line, those shaders get no translation and the tables differ from the profile's.
  • Unreal paks. An Unreal Engine game keeps its shaders in the shader code archives of its paks (*.ushaderbytecode; a pak is Unreal's when its footer has Unreal's magic, uepak.py unreal_paks), 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, found as the one function that references the library's own name string, 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; without them it stops). Octopath Traveler 0: 10,596 shaders in 2 s. Unreal Engine 4.26 and later compress shaders with LZ4 unless the project picks another format (DRAGON QUEST VII Reimagined, 4.27: 64,949 shaders, its packages in IoStore containers beside the pak); uepak.py decodes those itself (lz4_shaders) and the NSP script skips the host run.
  • Trinity packs. Game Freak's Trinity engine keeps nearly every file in Oodle-compressed packs (arc/data.trpfs, files starting ONEPACK; Pokémon Legends: Z-A). shader_split.py decodes them with the game's own OodleLZ_Decompress, run by the Mac host in its tool mode (dm/host/hle_oodle.c, DM_OODLE_SERVE). The split needs DM_OODLE_HOST (the Mac host) and DM_OODLE_MODULES (the rewritten modules); the NSP script builds the host and sets both when the split runs.
  • GLSLC outputs in main and loose files. shader_split.py reads GLSLC compiler outputs (magic 0x19866891, each one checked whole) in every game's main: 13 Sentinels and Unicorn Overlord keep all their shaders there, the KTGL games some; Unity's player has 11 of its own beside the game's shaders in its bundles (Engage, Ace Attorney Investigations Collection). It reads them as well in any RomFS file no other reader takes: FINAL FANTASY TACTICS - The Ivalice Chronicles keeps 257 in its technique files (resources_nx/release/master/shader/*.tec), Absolum (FNA3D) 163 in shaders.nx.bin.
  • MonoGame effects. shader_split.py reads MonoGame's MGFX effects wherever they are (mgfx_effect): Celeste keeps 13 in its XNB files (Content/Effects/*.xnb) and SpriteBatch's built-in one is in main.
  • Single-stage programs. A GLSLC output, an Unreal shader record or a MonoGame effect's shader is a program of one stage, combined with others per draw (a MonoGame pass may name no vertex shader and keep SpriteBatch's), so its real vertex / pixel pairs exist only at run time. shader_split.py pairs each such 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 it link with any pixel shader (13 Sentinels, Unicorn Overlord, the KTGL games, Octopath Traveler 0, Celeste). BNSH variations, Unity programs and the MSHD and SDF files pair their own shaders.
  • NVN driver tables. The driver's constant segments have one layout in every game (dmsh_build.py, filled by nvn_mtl.m): c2 holds the handles bound with nvnCommandBufferBindTexture at 0x20 + 8 × slot, images at 0x120 + 8 × slot, separate textures at 0x168 + 8 × slot and separate samplers at 0x568 + 8 × slot; c0 the storage buffer tables (vertex 0x110, compute 0x310, pixel 0x510). Separate texture binds are texture slots 128 + n of the host's table (NVN_SEPTEX_SLOT0), so a game may bind both kinds in one frame (Monster Hunter Rise: combined slot 0 beside separate slots 0-4), and a combined slot past 31 (Hades II binds slot 34) lies in the image table, where a bound texture wins over an unbound image. The translator (dmsh_xlate/Program.cs) numbers slots the same way when it looks up per-slot texture / image types in shader_facts.tsv.

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;
    • modules: a subsdk is loaded only when main imports something from it that the host does not supply itself. Under the Vulkan layer the Vulkan loader and GLSLC are dropped; movie modules and a GLSLC that main calls stay. NVIDIA's OpenGL and Vulkan drivers are never loaded: they program the GPU through NvRm, which the host does not have (Absolum imports EGL for SDL3's GL support and renders with NVN);
    • 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: Engage's 5 GB block with the hardware pool, which the heap check corrects when the game sets a heap (without one the game maps everything in the alias region itself: Hollow Knight: Silksong, Absolum, Pokémon Legends: Z-A);
    • 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); keep it when the profile moves into dm/games (Cuphead without it: a 0.03 GB heap and an abort at the first allocation). 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. A run with no heap set whose alias region is used up to within 0.25 GB of its end sets a 1 GB pool: nninitStartup handed pool − used to StandardAllocator as one address region and the game reserves regions of its own beside it (Unity 2020 and 2022: AI: THE SOMNIUM FILES - nirvanA Initiative reached +4.26 GB of 4.38 in 5 s with the hardware pool; Unity 2018.3's Raging Loop maps only inside StandardAllocator's region and keeps it). The 1 GB is a starting point, not a measurement: Advance Wars 1+2: Re-Boot Camp (Unity 2020.2) ran its malloc dry at its first loading screen with it and needs 2 GB. 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, c1_full.tsv and cb_extent.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 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. Shaders. DM_GAME=<id> [IOSOUT=…] dmsh_build.py <pkg> <cache>, with a cache directory of the game's own. A full build also writes c1_full.tsv and cb_extent.tsv (local files, not committed). The chain version needs nothing: it is the same for every game.

  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.