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).
20 KiB
dm/gfx — graphics layer (NVN → Metal)
The Switch version of the game draws with NVN (the Switch's native graphics interface). The graphics layer reimplements this interface on top of Apple's Metal: every NVN call the game makes lands here, is recorded as a command, and is replayed as Metal commands at submit time. The game's shaders are not translated at run time; they are translated offline, once, on a computer, into Metal shader libraries (when the computer has no Apple Metal compiler, the final compile step moves to the phone; see below).
For the build steps of the whole project see README.md and setup.sh in the repository root, and for dependencies see docs/DEPENDENCIES.md; this page covers only the graphics layer's own matters.
How it works
Taking over the NVN interface
- The game's main program imports only one NVN symbol,
nvnBootstrapLoader; all other NVN functions are obtained through it by name. The host forcibly takes over this import (nvn/hle_nvn.c), and not a single instruction of the original graphics driver is executed. - The game fetches the addresses of 523 names in total (
nvn/names.txt). Each name has a thunk (nvn/nvn_thunks.S, a generated file): it first increments that function's call count and then jumps to the implementation. - The functions games call have implementations, according to NVN semantics or empty (
g_nopsinhle_nvn.c); the rest are connected to a red-reporting stub that, when called, prints[RED] NVN unimplemented function called: <name>in the log. - Every implementation gets a compile-time signature check against
nvn/nvn_protos.h(a generated file). The calling conventions of Apple arm64 and the Switch differ for integer parameters narrower than 32 bits, so on the host sideNVNbooleanparameters are always received asuint32_tand only the low 8 bits are examined.
Two layers: record and replay
- Interface layer
nvn/hle_nvn.c: host-side objects are stored in place in the memory the game allocates for NVN objects; the layouts are innvn/nvn_obj.h, and each has a static assertion guaranteeing it does not exceed NVN's reserved size. Commands are recorded into the command memory provided by the game, in the record format ofnvn_obj.h. - Replay layer
nvn/nvn_mtl.m: at submit time it replays the record stream as Metal commands.- Memory pools are created as zero-copy
MTLBuffers, with the GPU address equal to the CPU address, so only unified-memory devices are supported. - The visible effects of "GPU completion" (sync objects becoming ready, events writing their value, counters) happen only after the Metal command buffer has actually completed.
- Pipeline states and depth-stencil states are cached by state hash; at startup, shaders and pipelines are pre-built in the background according to the warm-up manifests (the bundled seed
pso_manifest.tsv, then this device'spso_user.tsv; see below). A pipeline that fails to build is cached as failed and reported once with its shader keys. - The game's packaged textures are in Tegra block-linear layout and are deswizzled before upload (see
tex/README.md). - Presentation: the game's swapchain texture is drawn to a
CAMetalLayer, and presentation timing is scheduled by the pacer innvn/nvn_pace.h.
- Memory pools are created as zero-copy
For which Metal object each NVN object and command lands on, see nvn_metal_mapping.md.
Shader translation chain
The game's shaders are GPU machine code for the Tegra X1 (the Maxwell instruction set). The translation from this machine code to Metal Shading Language is not done on the phone; all of it happens offline:
Original Switch shader package
→ tools/dmsh_xlate produces SPIR-V with Ryujinx's shader translation library (a C# tool)
→ spirv-cross converts SPIR-V to Metal Shading Language (MSL)
→ xcrun metal / metallib compiles into Metal shader libraries
The whole chain is driven by tools/dmsh_build.py, and setup.sh runs it for you. Key points:
- Key: each shader is keyed by the first 16 hexadecimal digits of the sha256 of its control section (the first 0x880 bytes, or the section's own length at control +0x20 when that is shorter, as in SDK 10's 0x850-byte sections) followed by its code area (many of Mario's shaders share a control section and differ in code). At run time the game hands both to
nvnProgramSetShaders, and the replay layer computes the same key to find the library. - Build outputs:
<key>_b.metallib(the shader library),<key>_b.json(reflection used by replay: each binding's kind, slot number, index within the argument buffer and so on),<key>_b.metal(the MSL source). Some fragment shaders additionally have<key>_bf.metallib, a "readable tile" version used for pass merging (see the file header oftools/dmsh_fetch.py)._b_build.jsonin the directory records the success or failure of each key in this run. dm/games/<id>/shader_map.tsv: the shader map, one row per shader variant. At run time only its "key → stage (vertex / fragment / compute)" is used.dm/games/<id>/shader_facts.tsv: the answer table for translation queries. The translator needs to know some facts that cannot be read from the machine code (the dimension type of a texture, image formats, the component types of vertex attributes and fragment outputs, a compute shader's thread group size and shared memory byte count), and this table gives the answers by key. The table ships with the repository, and building the shader libraries only reads it; the table is maintained bytools/dmsh_facts.py, whose file header has the details.- Translation chain version and the
b-<V>/directory: the version V is a string of 12 hexadecimal digits, obtained by hashing together the contents of the source files involved in the translation chain, the translation options and the tool fingerprints (tools/dmsh_chain.lock);tools/dmsh_chain.pycomputes it. Shader libraries are written into<output directory>/b-<V>/, and every reflection json also records V. When the host is compiled it takes V fromdm/games/<id>/dmsh_chain.h, loads only fromb-<V>/, and checks the version in each json one by one; on a mismatch it reports red and does not load. So after changing any file on the chain, or changing a tool, the libraries must be rebuilt and the host recompiled.dmsh_chain.py: prints V.dmsh_chain.py --check-tools: checks that the local tools match the lock file.dmsh_chain.py --update-lock: rewrites the lock file according to the local tools.dmsh_chain.py --header: rewritesdm/games/<id>/dmsh_chain.h.- It is normal for the local Xcode or spirv-cross version to differ from the one registered in the repository;
setup.shautomatically re-registers according to the local tools.
dm/games/<id>/c1_full.tsv: the index list for the shaders' built-in constant section (c1), generated bytools/c1_guard.pyscanning the translated MSL; it lists the shader keys whose "run-time index could not be proven bounded". An index counts as proven only when interval arithmetic over the whole expression (every variable followed to its single definition; literals,& K,% K, shifts,min,max,clampand casts bound it) keeps it non-negative and inside the span a shared bank gives that shader: its c1 data rounded up to 256 bytes plus the 256-byte zero gap. Every stage counts (vp_,fp_,cp_…). Its first line records theb-<V>it corresponds to. For the shaders on the list the replay layer allocates a separate whole 64 KiB buffer for c1; when the list is missing or its version does not match, it reports red and conservatively treats all shaders this way.dm/games/<id>/shader_pairs.tsv: the (vertex key, pixel key) of every BNSH variation (Unity: of every program), written bytools/shader_split.py. Metal only builds a pipeline when every pixel shader input is a vertex shader output, while Maxwell gives an unwritten attribute a default;dmsh_build.pytherefore adds the outputs a vertex shader's partners read but it doesn't write, set to (0, 0, 0, 1) (vs_pad). The table is part of the translation chain version.- Several game versions:
shader_split.pytakes comma-separated RomFS andmainlists (base game first) and writes the union; the map, facts and pairs tables currently cover Mario v0 and 1.2.1. dm/games/<id>/pso_manifest.tsv: the warm-up seed manifest bundled with the app: shaders (S), render pipelines (R, the hex ofpso_skeyinnvn/nvn_pso_skey.h) and compute pipelines (C) to pre-build at startup. The row order is the pre-build order (first use); do not sort it. Its companion ispso_user.tsv, which the host appends next to the save directory whenever it has to build something on the spot, and which is pre-built after the seed at the next launch. Both name shaders by key and describe pipeline settings only, so a list recorded on one device works on any other; lines a build cannot use are skipped. Recorded lines can be appended to the seed below its header (currently empty).- Shaders embedded in the program itself: the main program additionally contains 4 compute shaders that are not in the shader package; they were hand-written as MSL from the disassembly and are in
nvn/nvn_builtin.h.
Binding conventions followed during translation (file header of tools/maxwell_refs.py):
- uniform buffer r is constant section
c[3 + r]in the machine code; - the handle of separate texture r is at
c[2][0x168 + 8r], and the handle of separate sampler r is atc[2][0x568 + 8r]; - the address of storage buffer r is at
c[0][base + 16r], with base: vertex 0x110, compute 0x310, fragment 0x510; - the window origin is at the top left and the depth range is 0 to 1, the same as Metal, so nothing is flipped during translation.
Directory structure
| Location | Contents |
|---|---|
nvn/hle_nvn.c |
Interface layer: NVN takeover, object implementations, command recording, presentation pacing |
nvn/nvn_mtl.m, nvn/nvn_mtl.h |
Replay layer: Metal device, memory pools, textures, shader loading, pipelines, drawing, presentation |
nvn/nvn_obj.h |
Host-side object layouts and the command record format, shared by both layers |
nvn/nvn_entry.S, nvn/nvn_thunks.S |
Entries for the unimplemented stub and the timing wrapper, per-function thunks |
nvn/nvn_tex.c, nvn/nvn_tex.h |
Storage size, alignment and per-level offsets of block-linear textures (ported from deko3d) |
nvn/nvn_dirty.c, nvn/nvn_dirty.h |
Tracks dirty pages of game memory with write protection, to decide whether texture contents have changed and need re-uploading |
nvn/nvn_refl.m, nvn/nvn_refl.h |
Converts shader reflection json into fixed-length C arrays, so that drawing only walks arrays |
nvn/nvn_hmap.h, nvn/nvn_cover.h, nvn/nvn_pace.h, nvn/nvn_pso_skey.h |
Hash table, geometric proof of full-coverage draws, presentation pacer, pipeline manifest key |
nvn/nvn_builtin.h |
Hand-written MSL for the 4 compute shaders embedded in the main program |
nvn/include/nvn.h |
NVN interface declarations (third party; for the license see the file header) |
nvn/names.txt |
The NVN API name list. The per-game tables described above and dmsh_chain.h live in the game profile dm/games/<id>/ (named after the base game's title ID; tools pick it with DM_GAME, make with GAME; there is no default game) |
tex/ |
Texture format mapping, deswizzling, BC decompression, see tex/README.md |
metal/ |
A standalone minimal Metal offscreen example, not compiled into the host, see metal/README.md |
tools/ |
Offline tools, see below |
tests/ |
Unit tests |
hle_vi.c, hle_nv.c |
Glue layer for display service and graphics driver initialization: returns host static objects or does nothing |
The scripts in tools/; usage is written in each file's header:
| Script | Purpose |
|---|---|
dmsh_xlate/ |
The shader translation tool (C#), which references the translation library in dm/third_party/ryujinx |
dmsh_build.py |
Drives the whole translation chain to produce the shader libraries; when the environment variable IOSOUT=directory is set, it also produces the iOS version |
dmsh_chain.py, dmsh_chain.lock |
Translation chain version and tool fingerprints |
dmsh_facts.py |
Generation and checking of the answer table shader_facts.tsv |
dmsh_fetch.py |
Generates the "readable tile" versions of fragment shaders |
c1_guard.py |
Generates c1_full.tsv |
dmsh_dryrun.py |
Dry-run conversion of the whole library, measuring only "can it be converted" |
maxwell_refs.py |
Reads resource references from the machine code |
gen_protos.py |
Generates nvn_protos.h / the thunks |
tbdr_budget.py, tbdr_load_dead.py, tbdr_store_dead.py, tbdr_pass_merge.py |
Read the DM_NVN_PASSDUMP log and analyze the load / store volume of each render pass's attachments per frame |
passgpu_top.py |
Reads the DM_NVN_PASSGPU output and lists GPU time by pass |
Building
The graphics layer has no separate library. dm/host/Makefile compiles dm/gfx/*.c and the source files under dm/gfx/nvn/ and dm/gfx/tex/ into the host dmhost by wildcard; the iOS App is built by dm/ios/tools/build_ipa.sh, which also copies the map, pso_manifest.tsv, c1_full.tsv and the b-<V>/ shader libraries into the App. tests/ and metal/ do not go into the host.
The shader libraries are generated by setup.sh:
- the Mac version is in
$DM_ROOT/build/cache/metal_shader_cache/b-<V>/; - the iOS version is in
$DM_ROOT/build/ios/dmsh_b/b-<V>/.
After changing a file on the translation chain, the order for rebuilding manually is the same as in setup.sh: dmsh_chain.py --header → dmsh_build.py → c1_guard.py → recompile the host.
The per-shader cache key in dmsh_build.py (shader_key) covers the profile's nvn_* / shader_* rows and the shader's pairs, and a paired vertex shader's key also takes its pixel shaders' keys (pair_keyed), since vs_pad reads their MSL. Before, these were only in the chain version, so a build after changing only them hit the old per-shader entries and filed them under the new version. $DM_ROOT defaults to ~/dm-work.
Tests
make -C dm/gfx/tests run # graphics layer unit tests
make -C dm/gfx/tex/tests run # texture submodule unit tests; the real-data items need texture samples from both the Switch and PC versions, see tex/README.md
make -C dm/gfx/metal run # offscreen example self-test, see metal/README.md
The tests in dm/gfx/tests compile the production code in directly and need a Metal device on the local machine; a few of them run under MTL_DEBUG_LAYER=1 (the Metal validation layer). Most tests do not read the workspace; the exceptions are t_refl and t_rt_range_ro, which read the translated shader reflection json under $DM_ROOT/build/cache/metal_shader_cache, and test_tbdr_budget.py, which creates a temporary directory under $DM_ROOT/run/gfx. Main contents:
| Test | What it checks |
|---|---|
t_nvn |
Interface layer: fetches addresses from nvnBootstrapLoader the way the game does, all the way through to presentation |
t_builtin |
The 4 embedded compute shaders |
t_refl, t_hmap, t_cover, t_pace, t_pso_skey, t_dirty_slots |
Reflection parsing, hash table, full-coverage proof, pacer, pipeline manifest key, dirty page tracking |
t_deswz_gpu |
GPU deswizzling |
t_present_crop |
Presentation cropping |
t_mip_extent |
Effective size when rendering to a mip level other than level 0 |
t_fuse_check, t_reorder_chain, t_defer_chain, t_zskip, t_xv, t_ub, t_pfv, t_rt_range, t_rt_range_shape, t_rt_range_ro, t_rt_depth_ro |
Compile the replay layer nvn_mtl.m in directly and check the various optimization and diagnostic logic inside the replay layer |
test_tbdr_budget.py |
The parsing in tools/tbdr_budget.py |
Common run-time switches
All are environment variables. The iOS App sets DM_NVN_SHMAP and DM_MSL_DIR itself at startup.
Resource locations:
| Switch | Effect |
|---|---|
DM_NVN_SHMAP=path |
The shader map. The default is ../games/<id>/shader_map.tsv relative to the executable's directory; c1_full.tsv and pso_manifest.tsv are taken from the same directory |
DM_MSL_DIR=directory |
The parent directory of the shader libraries; the host loads from b-<V>/ beneath it. When running the host on a Mac, set it to $DM_ROOT/build/cache/metal_shader_cache |
DM_PSO_MANIFEST=path or off |
Use a different warm-up manifest, or turn warm-up off |
DM_PSO_RECORD=path |
Appends the shaders and pipelines used in this run to a file, for re-recording the warm-up manifest |
DM_PSO_USER=path |
Where to keep the per-device list of shaders and pipelines built on the spot (default: pso_user.tsv in the parent of DM_SAVE) |
Logging and capture:
| Switch | Effect |
|---|---|
DM_NVN_LOG=1 |
Logs the creation of NVN objects |
DM_NVN_FRAMES=N |
Prints the call table frame by frame for the first N frames (default 3) |
DM_NVN_TSV=path |
Writes the per-frame call table and timings as TSV; adding DM_NVN_TSV_CALLS=0 writes only the timing rows |
DM_SLOWFRAME=1 |
Writes the timing breakdown into the log only for the frames that dropped; the attribution script is dm/ios/tools/slowframe.py |
DM_NVN_SHOT=frame[,frame…]:path |
Saves the swapchain texture as an image at the specified frames; %d in the path is replaced by the frame number |
DM_NVN_SHOT_OUT=WxH |
Makes the screenshot above capture the picture after upscaling to this resolution instead |
DM_NVN_TEXDUMP=frame:path |
Saves each color render target used in that frame as an image |
DM_NVN_PASSDUMP=frame or start-end |
Writes each render pass's attachments, samples and presentation for these frames to standard error entry by entry, for analysis by tools/tbdr_*.py; additionally setting DM_NVN_PASSDUMP_ALL logs every draw |
DM_NVN_PASSGPU=file |
Per-pass GPU time (requires device support for counter sampling at stage boundaries) |
DM_NVN_PROF=1 |
Wraps the implemented NVN functions in a timing wrapper and collects the time each function takes during recording |
Comparison and testing:
| Switch | Effect |
|---|---|
DM_NVN_TBDR=1 |
Turns on the pass optimizations of the replay layer (store / load elision, pass merging). They are off by default: they infer which textures a draw reads from its bound slots, which bindless textures defeat (docs/agents/04-graphics.md section 4) |
DM_NVN_UNWRITTEN_DC=0 |
With DM_NVN_TBDR=1, turns off the single item "attachments not written in this pass are not stored" |
DM_NVN_FUSE_CHECK=1 |
Same-frame check of pass merging; logs the comparison result every time |
DM_FPS=30 or 60 |
Pins the target frame rate |
DM_NVN_BARRIER=game, DM_NVN_CUT |
Pass ordering and segment commits: see docs/agents/04-graphics.md section 7 |
DM_PRESENT_HZ=N |
The frequency of presentation pacing (default 60); 0 means no frame limit and is for performance profiling only |
Known limitations
The following paths are not implemented and report red in the log when reached (RED_ONCE in nvn/nvn_mtl.m):
- clears in the middle of a render pass, and color clears with a partial mask or partial scissor;
- indexed quad primitives (8-bit indices are widened to 16-bit at replay);
- some forms of texture-to-texture copy and texture clear.
Functions listed in g_nops, such as MultiDraw*IndirectCount and SetDepthBounds, are empty implementations: calling them neither reports red nor has any effect. Downsample / DownsampleTextureView are recorded as texture copies with a host flag (DM_COPY_RESOLVE) and replayed as a Metal multisample resolve.