Hades (0100535012974000, update 1.0.38233, SDK 9) renders with Vulkan through The Forge, not NVN. The host now implements its 100 vk* imports on Metal in dm/gfx/vk/hle_vk.m, and the Vulkan loader (subsdk0) and NVIDIA's driver (subsdk1) are not loaded. The title menu, the intro and Tartarus gameplay run at 60 fps on the Mac, and a Metal validation run is clean. The iPhone build is untested. - dm/gfx/vk/hle_vk.m: one Tegra X1-shaped device. Memory is a lazily made shared MTLBuffer; images are private MTLTextures. Command buffers are recorded on the host and replayed at vkQueueSubmit on the layer's own queue, which signals an MTLSharedEvent. Descriptor sets, update templates, render passes and pipelines are supported. The swapchain presents through the NVN layer's present path. vkGet*ProcAddr look names up in the HLE table, also trying a name without its KHR suffix. - dm/gfx/tools/vk_shaders.py: SPIR-V to one metallib plus a slot table. It renumbers bindings per kind, flips vertex y, and lists only the resources the Metal entry point takes. - nvn_mtl.m/h: mtl_device, mtl_ext_tex and mtl_present_ext for external producers. Their present waits on the producer's event. - hle_nvn.c: dm_gfx_enter/leave and dm_vk_drain, so the background suspend also covers Vulkan submits. - hle_oe.c: GetNotificationMessageEvent returns a host-owned auto-clear SystemEvent. The SDK's version returned an event the no-op oe::Initialize never created. - patch_nso.py: the byte-table jump search looks 16 instructions back, not 8 (Hades main+0x1135d0). The output for every other module is byte-identical. - Profile key `graphics vulkan`: build_ipa.sh ships $MSL_IOS/vk instead of the NVN chain libraries. - dm/third_party/vulkan-headers: Vulkan-Headers 1.4.363, unmodified. - dm/.gitignore: per-game host binaries (dm/host/dmhost_*).
11 KiB
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, onekey<TAB>valuerow each, with comments on their own lines.- The shader tables that
shader_split.pywrites:shader_map.tsv(+.conflicts.tsv),shader_facts.tsv,shader_pairs.tsv. shader_facts_manual.tsv(optional): hand-written translation facts inshader_facts.tsv's row format, for what the machine code cannot tell (Link's Awakening: a vertex input the integer-input guess gets wrong).shader_split.pymerges 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 byc1_guard.pyafter each shader build.pso_manifest.tsv: the prewarm seed list.bf_want.tsv.dmsh_chain.h: the chain version, written bydmsh_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 listlists 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) |
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 |
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 |
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) |
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). Breath of the Wild's layout captures (panes tagged A8) keep a blurred alpha mask in the red channel of such an R8 texture, and every layout pane that samples one treats it as alpha; read literally, HUD prompts get hard-edged shadow boxes and cloud-filled glow rectangles. BotW lists all 26 pixel-shader variants of its layout library. DM_NVN_ALPHA_RED=0 / <keys> overrides, also from Documents/env.txt |
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 |
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) |
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
- Survey. Run
scripts/build_ipa_from_nsp.pyon 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.game.tsvthere is guessed from the survey: identity and name, the build row it found, all ExeFS modules, the largest RomFS file asromfs_marker, big file types inzip_store,shader_groupsby RomFS top-level folder, SDK 8 / 10 →shared_font ttfanddevice_api 53.313, Engage's memory partition when the NPDM declares a system resource and the 4 GB one otherwise, Mario's NVN values, andshader_main_glslc/shader_separablewhenmainholds GLSLC outputs and the RomFS has no shader containers. The shader tables,dmsh_chain.handc1_full.tsvare written into the same folder. A second run reuses thatgame.tsv(delete it to survey again), so it can be edited by hand. Tools find profiles outsidedm/gamesthroughDM_GAMES_DIR(folders separated by:, searched afterdm/games);make -C dm/hostreaches such a profile through the symlinkdm/host/gen/<id>/profile. To keep a game, move the folder intodm/games/and bring it up as below. - Profile. Copy
010015100B514000/game.tsv(or the provisional profile) todm/games/<title ID>/game.tsv(the base game's title ID, as the folder name) and fill in the identity,buildrows andmodules. Leave the memory and NVN values as Mario's until the bring-up measures them (docs/agents). - 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 emptyshader_map.tsv.conflicts.tsv,bf_want.tsvandpso_manifest.tsv(copy Mario's headers). - Chain.
DM_GAME=<id> dmsh_chain.py --headerDM_GAME=<id> [IOSOUT=…] dmsh_build.py <pkg> <cache>c1_guard.py <cache>/b-<V> dm/games/<id>/c1_full.tsv
- 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. - IPA. Use
GAME=<id> dm/ios/tools/build_ipa.sh, or the NSP script, which now finds the profile.