Incorporate the active SVC improvements from PR #151 around the existing alias bitmap and mapping registry. Serialize heap changes, physical maps, stack ownership and memory queries with one lock; report committed alias runs and borrowed sources; initialize reused logical pages while preserving live neighbors and lazy first exposure. Retain PR #127's fail-fast host protection policy and the log formats parsed by the NSP heap probe. Game smoke tests exposed two integration requirements: Raging Loop's SDK extends committed windows across borrowed stack sources, so allow recommit that preserves existing bytes; Tears of the Kingdom's audio helper needs guest-backed stacks, so allocate host-created guest-thread stacks through SDK malloc and free them after thread destruction and copyback. Keep SDK reservation ownership and existing patches. The unused host reservation allocator from #151 is not included. Validation: full host suite; focused UBSan, ASan and ThreadSanitizer tests; real dirty-tracker concurrency tests; heap-probe parser compatibility; Mac host builds; iOS arm64 syntax checks; all 60 profile layout checks. Raging Loop and Tears of the Kingdom passed guest-thread self-checks and ran to deliberate timeouts after the startup fixes. Their shader caches are stale for this checkout, so rendering/performance are not validated. No IPA build or iOS device validation was performed. Co-authored-by: b4f8abbb24dbf56a7d0280f296e16eeb <51352400+b4f8abbb24dbf56a7d0280f296e16eeb@users.noreply.github.com>
217 KiB
Development
The README covers packing and playing. This page is for working on nxnative itself: how it is built, the script's options, running games on the Mac, adding a game, and per-game notes. Agent-oriented details (bring-up failure catalogue, graphics specifics, debugging recipes) are in docs/agents.
How it works
| Part | State |
|---|---|
Rewriter (dm/tools/patch_nso.py) |
Runs unchanged on every module of every supported game. Per-build cuts are keyed by the module's build ID, in the profile's patches.py (a game's own modules) or dm/tools/sdk_patches.py (Nintendo's sdk modules) |
Host (dm/host) |
Stands in for the Horizon kernel and the system services the games call: threads and synchronization, memory, file system with transactional saves, HID, audio, movies (VP9 through libvpx, Vorbis sound through stb_vorbis), software keyboard, focus and lifecycle |
Graphics (dm/gfx) |
NVN on Metal. Shaders are extracted by shader_split.py from Nintendo's containers and from Unity's Shader objects, then translated offline. Mario: 16,266 of 16,268 shaders compile. Engage: 9,268 of 9,269. Games that render with Vulkan (Hades) or OpenGL (Digimon Story Cyber Sleuth) go through layers of their own on Metal (dm/gfx/vk, dm/gfx/gl) |
| Audio | Games that carry the SDK's audio renderer (Mario) run it in-process; games that use AudioOut directly (Engage: Wwise) feed it straight. Output goes through Core Audio (default on iOS, DM_AUDIO=1 on the Mac) |
| Input | iOS: the touch gamepad and physical controllers, all as player 1; shaking the phone shakes the controller (its six-axis sensor, hle_hid.c; the accelerometer in DmGamepad.m, threshold DM_SHAKE_G, 1.5 G by default). Mac: scripted input (DM_KEYS=<seconds>:<button>,…, Shake for a shake, with DM_KBD=<text> answering the system keyboard) or DM_WINDOW=1 for a window with keyboard input (Space shakes) and physical controllers (buttons by position, as on the iPhone). On the Mac each controller is a player of its own, for local co-op: the first one connected is player 1, together with the keyboard, the next player 2, and so on up to 8 (pad_slots.h); a controller that is removed frees its player and gets it back when it reconnects, and the log names each one's player and npad (controller 'Xbox Wireless Controller' connected: player 2 (npad 1)). Player 1 is the Handheld npad in handheld mode and No1 docked, player n is No n; the game's supported npad ids decide how many play (hle_hid.c): a one-player game gets every controller as player 1, and a controller past a game's last player is not used. DM_PAD_MERGE=1 makes every controller player 1, as before; DM_KBD_PLAYER=own makes the keyboard player 1 on its own, with the controllers from player 2 on. The window goes fullscreen from its green button or ⌃⌘F; DM_FULLSCREEN=1 starts it that way. In fullscreen, a screen whose refresh rate is not a multiple of the game's (144 or 165 Hz) runs at one that is (120 Hz) until the window leaves fullscreen; DM_SCREEN_MATCH=0 keeps its rate. The drawable follows the window in pixels (fullscreen at the screen's resolution); DM_METAL_HUD=1 shows Apple's performance HUD (on the iPhone too, from Documents/env.txt). The left mouse button is the touchscreen, on the 16:9 picture. In handheld mode the touches also drive hid::GetGestureStates (taps, presses, pans, swipes, pinches and rotations recognized by the host, hle_touch.c) |
| iOS app | One unsigned IPA for iPhone and iPad. The rewritten code is embedded in the executable; the shader libraries, the RomFS and the game icon are inside the app. A settings screen comes before the game (Launcher.swift, drawn from dm/launcher.json, which dm/games/settings.py writes from dm/games/settings.tsv); the host's loading starts at app launch meanwhile, and the frame rate and render scale go into the frame step state when the game starts (loader.c dm_fps_state_apply) |
| Known gaps | Conditional render is always drawn; MultiDraw*IndirectCount waits on the CPU for the GPU-written count; geometry shaders do not translate. TBDR optimizations are off by default (they don't understand bindless textures yet). No amiibo. The bundled prewarm lists are empty. On the Mac, Mario's menu and dialogue text render blank (fine on the iPhone) |
Guest memory SVCs
dm/host/hle_svc.c serializes heap resizing, physical map/unmap, stack
map/unmap and memory queries with one memory-state lock. The alias bitmap is
the source of committed/free runs for QueryMemory and of committed-byte
accounting. With mem_map_limit, heap growth and physical maps check the same
application-pool budget; overlapping maps charge only newly committed pages.
Unexpected host protection failures abort before publishing state, including
the SetHeapSize fail-fast check from PR #127.
MapMemory borrows opened heap memory, committed alias memory or writable
static-module data until an exact unmap copies it back. Another borrow,
decommit, heap shrink or permission/attribute request cannot invalidate that
source. Queries report its original state with borrowed attribute bit 0 and
no user permissions. The implementation still copies bytes: source permissions
are logical, and native host pages cannot isolate adjacent 4 KiB slices.
Dynamic NRO backing is rejected because reload can restore it outside this
ownership lifetime. Host-created guest threads allocate their stack backing
through SDK malloc and free it after SDK thread destruction/copyback. Physical
recommit may overlap a borrowed source: SDK allocators extend committed windows
this way, and existing committed bytes are preserved.
A host-page exposure bitmap preserves lazy zero-fill on first commitment. Later commits clear only newly committed logical slices, preserving live neighbors on a 16 KiB host page. GPU dirty tracking is notified when a mapping opens a host page and before stack copyback. The SDK retains address-region reservation ownership and its existing build patches.
make -C dm/host/tests GAME=<id> run includes the production-dispatcher memory
tests, real GPU dirty-tracker integration and heap-probe log compatibility.
The focused targets are heap_size, memory, memory-dirty, memory-profile
and memory-tsan; memory includes undefined-behavior and address sanitizers.
Use PYTHON=<venv>/bin/python when the NSP tools' Python dependencies are in a
virtual environment; the heap-probe check imports the production profile reader.
The packing script in detail
scripts/build_ipa_from_nsp.py (-h lists every option):
# written to the Desktop with your own home-screen name
python3 scripts/build_ipa_from_nsp.py ~/Games/MarioWonder -k ~/keys/prod.keys -o ~/Desktop/Mario.ipa --app-name "Mario Wonder"
# pack Mario 1.0.0 instead of 1.2.1 (the folder still needs both NSPs: the shader tables cover both versions)
python3 scripts/build_ipa_from_nsp.py ~/Games/MarioWonder -k ~/keys/prod.keys -p v0
# delete the extracted game files after the IPA is built (they are kept by default, to skip extraction next time and to run the game on the Mac)
python3 scripts/build_ipa_from_nsp.py ~/Games/MarioWonder -k ~/keys/prod.keys --no-keep-extracted
# play the game in Japanese (a code or a name: ja, fr, de, es, zh-Hans, zh-Hant, ko, en-GB, ..., Japanese; English by default)
python3 scripts/build_ipa_from_nsp.py ~/Games/MarioWonder -k ~/keys/prod.keys -l ja
# show the game's frame rate on the iPhone (frames on screen per second, top left)
python3 scripts/build_ipa_from_nsp.py ~/Games/MarioWonder -k ~/keys/prod.keys --show-fps
# a diagnostic build with environment switches baked in (one KEY=VALUE per line)
python3 scripts/build_ipa_from_nsp.py ~/Games/MarioWonder -k ~/keys/prod.keys --env-file diag_env.txt
# a damaged NSP (files failing the NCA's hash check) stops the build after listing them; this packs those files as read
python3 scripts/build_ipa_from_nsp.py ~/Games/MarioGalaxy -k ~/keys/prod.keys --allow-damaged
# a game without a profile: survey, provisional profile (a heap check on the Mac when the game needs one), best-effort IPA
python3 scripts/build_ipa_from_nsp.py ~/Games/NewGame -k ~/keys/prod.keys
# test the guesses on a game that has a profile: build from a provisional one (the IPA is named <ipa>-provisional.ipa)
python3 scripts/build_ipa_from_nsp.py ~/Games/MarioGalaxy -k ~/keys/prod.keys --provisional --allow-damaged
# development: build only what running the game on this Mac needs (no iOS shaders, Xcode project or IPA) and print the command that runs it
python3 scripts/build_ipa_from_nsp.py ~/Games/MarioWonder -k ~/keys/prod.keys --platform mac
In Spyder, open the script, fill in the settings at the top (NSP_DIR, PROD_KEYS, and optionally PACKAGE, OUT_IPA, APP_NAME, BUNDLE_ID, LANGUAGE, KEEP_EXTRACTED, SHOW_FPS, PLATFORM; for a game without a profile HEAP_CHECK, HEAP_CHECK_SECS, PROVISIONAL), and press F5. Empty paths are asked for at run time.
What the script does:
- It finds the game profile from the title ID (the
[title ID]in the file names, or the NSP's ticket; the update by its[vN]tag). The update is optional: it packs the newest version in the folder (-p/PACKAGEpicks another). - It extracts the NSPs.
- It rewrites the code modules, checking each build ID against the profile. A version the profile does not register (the base game alone when the profile was brought up on an update, another update, or files whose build IDs differ), or one whose shader tables need a version that is not in the folder, is built from a profile derived from it in
<id>_build/profile/<id>/: the profile's keys, that version's build row, and shader tables, chain header,c1_full.tsvandcb_extent.tsvmade from that version's own files (as for a provisional profile, without the heap check). The rewriter's cuts and the host's fixes for a registered build do not reach a module of another build; the script says which modules those are. A title ID no profile has is first looked up by build: when itsmain(of the base game with its newest update, else of the base game alone, read from a code-only extraction) is a build a profile registers, a regional release of the same build (Advance Wars 1+2: Re-Boot Camp under0100300012FFF000, issue #114), it packs from a profile derived from that one, with the folder's title ID andmain.npdm's program ID (same_build,game.pyby_build); only otherwise is it surveyed. A provisional profile (a game without one indm/games) then getsmem_usedfrom the rewritten modules, and, when the game has virtual address memory and sets a heap, a heap check: the Mac host is built into<id>_build/tools/dmhostand the game boots in it until its first heap request (a few seconds, log<id>_build/logs/heap_check.log), which sizes the heap region (see dm/games/README.md, Adding a game). - For a game whose
mainholds GLSL sources and callsglslcCompile(Three Houses), it captures the shaders the game compiles at run time, which no game file holds. It builds the Mac host into<id>_build/tools/dmhostand boots the game in it, headless and silent, withDM_GLSLC_CAPTUREpointing at<id>_build/extract/<version>/glslc_capture. The save, temporary storage and log all stay in<id>_build/. It stops the game as soon as every capture file named in the profile's shader map is there (Three Houses: 9, about a second after launch), and gives up after 30 s. A folder that already holds them all is reused. A Vulkan game without SPIR-V files in its RomFS (Super Mario Galaxy) gets the same kind of run: the game boots in the Mac host withDM_VK_CAPTUREpointing at<id>_build/extract/<version>/vk_captureuntil every SPIR-V key in the profile'svk_capture.tsvis there (Galaxy: 3,089, about 7 s after launch), and gives up after 120 s. So does an OpenGL game with no GLSL source in its RomFS ormain(Balatro: LÖVE puts its shaders together at run time, in Lua):DM_GL_CAPTUREat<id>_build/extract/<version>/gl_captureuntil every file in the profile'sgl_capture.tsvis there (Balatro: 35, made before its first frame). Without the profile's tables (a provisional profile) a capture stops once nothing has been added for 20 s. The Vulkan and GL captures also run on a Mac whose GPU has no Metal 4, such as a virtual Mac (GitHub'smacos-26runner). Those two layers draw on classic Metal queues of their own, so there the NVN layer starts only its classic part and drops every present, which a headless capture does not need (nvn_mtl.mmtl_device_classic;DM_TEST_NO_METAL4=1does the same on any Mac). The GLSLC capture boots an NVN game, which needs Metal 4. - It splits out the shaders and checks them against the profile's tables (a Vulkan profile: it translates the RomFS's SPIR-V and the capture with
vk_shaders.pyinstead; an OpenGL one: the GLSL sources and the capture withgl_shaders.py). - It builds the iOS shader libraries, with the Mac's own Metal compiler and spirv-cross when they differ from those in
dmsh_chain.lock(dm/gfx/tools/own_compilers.py; one line in the output names them). If a registered profile'sdmsh_chain.hdoes not hold the version the source tree gives (a chain change left it behind), the script writes it again before any host is built, so it shows as changed in the checkout (dmsh_build.pywould stop otherwise). Then the script makes the profile'sc1_full.tsvandcb_extent.tsvfrom this build's Mac outputs when they are missing or name another chain. These two tables are not committed and git ignores them;build_ipa.shstops without a currentc1_full.tsv. Each line in the output names the file. - It calls
build_ipa.sh. Xcode builds the app in the game's own build folder (<id>_build/ipa/build/ios/pkg/dd:build_ipa.shsetsSYMROOTandOBJROOT, which a custom build location in Xcode's settings does not override), so a repeat build compiles only what changed and two games built at once never write the same app. Thendm/ios/tools/pack_ipa.pypacks it with the shader libraries, the add-on content and the RomFS straight from where they are. It reads and deflates files on all cores and writes at about the T7's write speed (300 MB/s): Breath of the Wild's 16 GB IPA takes 57 s, Info-ZIP's copy and zip took 2 min 50 s.
A repeat build skips work whose inputs have not changed. The extraction is kept (extract/). The DLC, the rewrite, the shader split and the iOS (or Vulkan) shaders each write a key beside their output: a sha256 of their inputs (the game files, the tools, the profile, Xcode's and spirv-cross's versions; for the DLC, the DLC NSPs' names, sizes and times and nx_extract's sources). When the key matches, the stage line says reused (inputs unchanged) and the step does not run. Any change to a tool or the profile runs the step again; deleting the .key file forces it. The iOS shaders' inputs are the shader package's content and only the tools dmsh_build.py loads (the modules it imports, at any depth): a split that runs again because a reader for another engine changed (a pull that touches shader_split.py or unityfs.py) writes the same package, and the iOS shaders stay reused.
The rewrite also collects NRO modules registered by the game's NRR files, including modules inside supported archives (nro_collect.py). The host maps the rewritten modules at startup and activates them on nn::ro::LoadModule, with symbol precedence following activation order; unloading and reloading restores their initial data and bss. DM_NRO=0 disables this loading. Epic Online Services modules are identified by their exports and refused, preserving offline play. Collection reuse includes the archive reader and symbol parser; the rewrite key includes nro.tsv, even when it lists only refused modules. On iOS the rewritten NROs are packed into three sections of the executable (dm/ios/tools/gen_embed.py: a Mach-O file has at most 255 sections, and Super Smash Bros. Ultimate's 98 NROs would take three each) with a table of their pieces, from which the host maps them at start (loader.c nro_pieces). The synthetic loader test (make -C dm/host/tests GAME=<id> nro) covers load order, rebinding, weak symbols, init/fini, data reset and refusal without game files.
While it runs, the console shows one line per stage: building nx_extract, extracting each version, the DLC, the rewrite, the run-time shader capture (only for such profiles), the shader split, the iOS shaders, the app build and packing the IPA. Each line shows [k/n], the stage and its current step. Where the work can be counted, it adds a bar with done/total, percent, elapsed time and ETA. Counted work is bytes extracted, modules rewritten, shaders (cache check, translation, compile), files compiled by Xcode, and bytes packed into the IPA. Elsewhere a spinner shows the elapsed time. A total marked ~ is an estimate. A version's first extraction is estimated from the NSPs' size, and later extractions use the previous run's exact size. The IPA is estimated from the previous IPA, or from the files' size before compression. In a terminal or in Spyder the line is redrawn in place. When the stage ends, it becomes a ✓ line with the elapsed time and a summary:
[15:24:28] ✓ [2/7] extract v327680 (0:20): 9,779 files, 5.0 GB
⠸ [7/7] IPA pack · packing ████████▍░░░░░░░░░░░ 42% 1.9/~4.5 GB · 13,597 entries 0:05 · ETA 0:07
When the output goes to a file or a pipe, the script prints a plain line instead, every BEAT seconds (30). --progress bar|lines|off (PROGRESS in Spyder) overrides that choice; off prints only each stage's start and end. The tools' own output goes only to the logs in <id>_build/logs/, one per step. When a step fails, the script prints the last 40 lines of that step's log and the log's path.
--progress json is for a program that shows the build (a GUI): stdout carries one JSON object per line, each with ev and the time t. The events are stage (k, n, name, step), step, progress (done, total, unit, est, note, frac, eta, elapsed; null while unknown), stage_end (ok, summary, elapsed) and say (msg: every message, errors and a failed step's log tail included). A count change goes out at most every 0.5 s, any other change at once. This mode never asks for the NSP folder or the keys; argument errors and an unexpected error's traceback still go to stderr. dm/tools/tests/test_nsp_progress.py tests it without game files, keys or Xcode (python3 -m unittest discover -s dm/tools/tests).
Everything it writes stays in the NSP folder: intermediate files and logs in <id>_build/, the IPA next to the NSPs. NSZ and XCZ files are converted with nsz -D (from the nsz package in the Python running the script) into <title ID>_build/nsp/, through a temporary folder so that only a conversion nsz finished is used, and reused while the source keeps its name, size and modification time; nsz -D reports an NCA hash mismatch but goes on, so a damaged file is caught by nx_extract's own hash check (--allow-damaged). The extracted game files stay in <id>_build/extract/ for the next build; --no-keep-extracted (KEEP_EXTRACTED = False in Spyder) deletes them at the end. It exits with 0 when done and 1 on an error. The app's bundle ID is org.nxnative.<base game title ID> (for example org.nxnative.0100a6301214e000), and its version is the game's display version: the update's when an update is packed, else the base game's (Engage 2.0.0); --app-name / --bundle-id (or APP_NAME / BUNDLE_ID in Spyder) override the name and ID.
The game language (--language, LANGUAGE; English by default) is what nn::oe::GetDesiredLanguage returns. As on the console, it must be one the game's control data lists (SupportedLanguageFlag, nacp.py --languages); the packed version's control data counts, since an update may add languages. The same language for another region stands in (en-GB for en-US, es for es-419, fr for fr-CA, pt for pt-BR, and back). A language the game lacks stops the build with the game's list; English, the default, falls back to the game's first language.
On the device, environment switches go in Documents/env.txt (one KEY=VALUE per line), and game files are also accepted at Documents/romfs.
Game rumble is enabled by default (DM_RUMBLE=0 disables it); DM_RUMBLE_GAIN=0..3 scales its strength (default 1, zero is silent). Both are launcher settings, independent of DM_TOUCH_HAPTICS. hle_hid.c decodes SDK vibration handles (style index / npad / motor bytes, as in libnx's _hidGetVibrationDeviceHandles) and routes them through the same player assignments as input. hle_haptics.c retains each motor's two amplitude/frequency bands, commits batches atomically and invalidates queued output on lifecycle or route changes. haptics_apple.m provides shared Core Haptics output for Mac controllers and iOS: left/right handle localities when available, otherwise one combined output; on iPhone, the phone is used while no controller is selected. A connected controller without haptics produces no game rumble. Continuous effects keep running until the game stops them; short effects are not extended. Engine resets, focus loss and interruptions clear the old values and require a fresh game signal. The frequency-to-sharpness mapping is approximate and physical feel / controller compatibility require device testing. The headless Mac host has no output. make -C dm/host/tests GAME=<id> run covers state and HID routing without hardware.
Haptic engines are created on demand, after the game has activated its audio session. Stop/reset handlers are installed before starting an engine. Idle release, focus loss and engine resets stop an engine but keep it, so the next effect restarts the same one. An engine or player failure releases that connection; fresh game signals retry with a new engine after a 1–8 second backoff, without replaying effects from before the failure. If a controller advertises separate handle localities but those engines cannot be created or started, the route falls back to the controller's default locality until it is reconnected. The log names the controller, localities and output channels, and errors name the failed stage with the error domain/code and underlying service error. The Apple adapter test covers failed creation/start, player and parameter failures, default-locality fallback and stale callbacks using fake controllers and engines; an iOS Settings Identify response alone does not verify the app's own haptics connection.
A Mac-only build for development
The Mac is the development host, not a platform for players: there is no Mac app. To test or debug a game on the Mac, --platform mac (PLATFORM = 'mac' in Spyder) builds only what running it in the Mac host needs, from the same NSP folder:
- the same steps up to the shaders: extraction, rewrite, run-time shader captures and decodes, a provisional profile's heap check and the shader split;
- the Mac shader libraries only:
dmsh_build.pywithoutIOSOUTinto<id>_build/shader/cache/b-<chain>/, which a full build stamps with the profile's inputs (inputs.tsv) and from which it makes the profile'sc1_full.tsvandcb_extent.tsv, or a Vulkan or GL game's library without--iosinto<id>_build/shader/vk/orgl/; - the Mac host for the profile into
<id>_build/tools/dmhost: the compile line ofmake -C dm/host GAME=<id> dmhost, with a provisional or derived profile throughDM_GAMES_DIR/DM_GAMES_FIRSTas for the script's other host builds. It is reused while its sources (the chain headerdm/gfx/nvn/dmsh_chain.hamong them), the generated profile header, libvpx and the compiler are unchanged; - the button-icon font (
make_ext_font.py) into<id>_build/font/.
It skips the iOS shaders, the Xcode project and the IPA. For Mario Kart 8 Deluxe that saves about 30,000 iOS shader compiles and the 12 GB IPA. It keeps the extracted files even with --no-keep-extracted, because the game runs from them. It needs a Mac with Apple silicon, and it cannot be combined with --prepare-xcode. The last line it prints is the command that runs the game, built from the run's own paths (step 5 of Build by hand (Mac)), for example:
cd "<NSP folder>/<id>_build/run" && DM_ROOT="<NSP folder>/<id>_build" DM_ROMFS="<NSP folder>/<id>_build/extract/<version>/RomFS" DM_SAVE="<NSP folder>/<id>_build/run/save" DM_TEMP="<NSP folder>/<id>_build/run/temp" DM_MSL_DIR="<NSP folder>/<id>_build/shader/cache" DM_FONT_DIR="<NSP folder>/<id>_build/font" DM_NVN_SHMAP="<repository>/dm/games/<id>/shader_map.tsv" DM_LANG=en-US DM_WINDOW=1 DM_AUDIO=1 "<NSP folder>/<id>_build/tools/dmhost" "<NSP folder>/<id>_build/patched_<version>"
The save is in <id>_build/run/save and stays there from run to run. DM_AOC_DIR is added when the folder has DLC. DM_NVN_SHMAP is added for an NVN game; it names the profile's shader map, or the derived or provisional profile's. A Vulkan or GL game's DM_MSL_DIR is <id>_build/shader. The switches of --env-file are added to the command.
Reuse works as for the IPA. Each shader output has its own key, named after the one chain version: shader/ios/b-<chain>.key for the iOS libraries and shader/mac-b-<chain>.key for the Mac's (shader/ios/vk.key and shader/vk.key for a Vulkan game, the same with gl for a GL game). An iOS build writes both keys, since it builds the Mac's libraries too, so a Mac-only build of a folder already packed as an IPA reuses its shaders. Either key is taken only while the Mac libraries carry a full build's stamp; c1_full.tsv or cb_extent.tsv that does not name it is made again from them, as for the IPA. An iOS build after a Mac-only build still has the iPhone's libraries to make. It translates the shaders again, because dmsh_build.py's per-shader cache keys include the iOS compile convention. It then takes the Mac libraries from the Metal compile cache (shader/cache/cas/metallib) and compiles only the iPhone's.
Build and run on a device with Xcode
With an Apple developer account, Xcode can sign and install the app itself:
python3 scripts/build_ipa_from_nsp.py "<NSP folder>" -k "<prod.keys>" --prepare-xcode
open "<NSP parent folder>/NxNative.xcworkspace"
--prepare-xcode (PREPARE_XCODE = True in Spyder) runs the same extraction, rewrite and shader preparation, reusing unchanged outputs, then stops after generating the project. It does not build an IPA, sign, or install. It needs the extracted files, so it cannot be combined with --no-keep-extracted.
- In Xcode > Settings > Apple Accounts, add your Apple developer account.
- Open the DmIOS target's Signing & Capabilities, enable Automatically manage signing, and select your team, replacing the generated placeholder. If Xcode reports that the bundle identifier is unavailable, choose an identifier you own (or pass
--bundle-idwhen preparing). - Connect and trust your iPhone or iPad, enable Developer Mode on the device, and select it as the run destination. The iOS Simulator cannot run the game's code.
- Select the game's scheme, named Game name (title ID), and press Run. Its Run configuration is Release. Xcode manages provisioning, signs the app and installs it on your device.
Each game has its own generated project at <NSP folder>/<title ID>_build/xcode/DmIOS.xcodeproj. Its Copy game resources target phase (dm/ios/tools/stage_resources.sh) runs before code signing: it syncs the RomFS, the iOS shader libraries and the add-on content named in the project's Gen/resources.env to DmIOS.app/romfs, DmIOS.app/msl/<b-chain, vk or gl> and DmIOS.app/aoc. The rewritten game code is embedded by Gen/embed.S at link time. No keys are copied to the device. The project links Sources, tools and the host sources to the checkout it was prepared from (relinked on each preparation), so code edits apply to every prepared game. The prepared shaders do not follow a chain change: when the profile's dmsh_chain.h names another chain than the prepared shader folder (after a pull, or dmsh_chain.py --header), the phase stops and asks to prepare the game again.
The project asks for the Increased Memory Limit capability (Gen/DmIOS.entitlements; without it iOS closes the bigger games, see the README). DM_ENTITLEMENTS=<plist> when preparing uses another entitlements file, and an empty DM_ENTITLEMENTS= none, for an account that cannot provision it.
The first Xcode build copies the complete game, so allow free space on the game's drive for another uncompressed copy; later builds sync changes. Derived data and build products go under <NSP folder>/<title ID>_build/xcode-derived-data/; keep that drive connected while building. The installed app holds its files and runs without the Mac.
Each prepared game is a scheme in the shared workspace (by default NxNative.xcworkspace beside the NSP folder; --xcode-workspace, XCODE_WORKSPACE in Spyder, collects games from different folders in one). Give each game its own bundle ID (--bundle-id) to keep them installed together; keep it when updating an installed game, as another ID is a separate app with separate saves. The script rejects a bundle ID that another project in the workspace uses. Preparing a game again regenerates only its project and keeps its Release bundle ID and team, including changes made in Xcode; --bundle-id / --team-id (BUNDLE_ID / TEAM_ID in Spyder, or DM_TEAM_ID) override them. Other manual project edits are replaced; edit dm/ios/project.yml for shared changes. Prepare a game again after changing its game files, profile, shaders or the project template; switching games needs only the scheme menu.
The unsigned IPA build still uses the repository's dm/ios/DmIOS.xcodeproj, skips the resource phase (DM_COPY_GAME_RESOURCES=NO) and packs the resources straight from the drive as before.
Port another game
Each game has a profile in dm/games/<id>/:
game.tsvholds the title ID, known builds, code modules, RomFS marker, memory split, app name and bundle ID;- the shader tables:
shader_map.tsv,shader_facts.tsv,shader_pairs.tsv,pso_manifest.tsv, andc1_full.tsv/cb_extent.tsv, which each checkout's full shader build makes (not committed); patches.py(when there are any): the rewriter's patches of the game's own builds.
The shader translation chain has one version for every game, in dm/gfx/nvn/dmsh_chain.h.
The shader tools pick a profile with DM_GAME=<id>; make -C dm/host and build_ipa.sh pick it with GAME=<id>. Profiles are named after the base game's title ID. There is no default game: without GAME / DM_GAME the tools stop and list the profiles. python3 dm/games/game.py list lists the profiles. Host code that relies on one build's fixed addresses checks that build's ID.
To add a game:
- Run
scripts/build_ipa_from_nsp.pyon its NSP folder. With no matching profile, it writes a survey to<title ID>_survey/survey.md. The survey covers the modules, SDK, engine, containers, rewrite sites, and the imports compared with the host's hooks. It then writes a provisional profile into<title ID>_build/profile/<title ID>/(outside the repository; tools find it throughDM_GAMES_DIR) with the keysdm/games/profile_guess.pyreads off the game files: the graphics API, the modules to load, where the shaders come from, the memory partition, SDK and engine presets. After the rewrite it setsmem_usedfrom the rewritten modules and, for a game with virtual address memory that sets a heap, boots it on the Mac until its heap request (the heap check, a few seconds) to size the heap region. Then it generates the shader tables and chain header and packs a best-effort IPA that may not boot.--survey-onlystops after the survey,--no-heap-checkskips the heap check, and--profile-outputs the profile elsewhere.--provisionaldoes all this for a game that has a profile, to test the guesses;python3 dm/games/profile_guess.py --checkcompares the static guesses with every profile. Details in dm/games/README.md. - Add a profile as described in dm/games/README.md.
- Bring the game up following docs/agents: extraction, binary survey, a catalogue of host bring-up failures, graphics, and debugging recipes.
Build by hand (Mac)
scripts/build_ipa_from_nsp.py --platform mac does steps 1 to 4 in the game's NSP folder and prints the command of step 5 (A Mac-only build for development). The steps by hand follow. You need the same tools as the script. The shader translator is the prebuilt dm/gfx/tools/dmsh_xlate_bin, the build that matches dmsh_chain.lock; a fresh dotnet publish gives different fingerprints. Its launcher dmsh_xlate is a universal binary (arm64 and x86_64), so an Intel Mac runs it too, with the same output; after a rebuild, join the new launcher with lipo to the one dotnet build -r osx-x64 makes of the same .NET version (the lock does not cover the launcher). What an Intel Mac cannot run is the Mac host, which runs the game's own arm64 code: the NSP script stops at a step that boots the game there (a run-time shader capture, a decode with the game's own Oodle or LZ5) and skips a provisional profile's heap check. The commands use three variables:
G, the profile;W, the game's work folder:<title ID>-workinside the game's NSP folder, outside this repository and off the home folder (it grows to the size of the game files);VER, a version folder named after the NSP version.
G=010015100B514000; W="<NSP folder>/$G-work"; VER=v327680
# 1. extract: the base game alone (update -), or an update layered over it (the last argument lets unchanged files become clones)
dotnet build dm/tools/nx_extract -c Release -o $W/build/nx_extract
$W/build/nx_extract/nx_extract <prod.keys> <base NSP> - $W/data/v0
$W/build/nx_extract/nx_extract <prod.keys> <base NSP> <update NSP> $W/data/$VER $W/data/v0/RomFS
# 2. rewrite the profile's code modules
for m in $(python3 dm/games/game.py $G modules); do python3 dm/tools/patch_nso.py $W/data/$VER/ExeFS/$m $W/build/patched_$VER $m; done
# 3. shaders: extract from every version in the profile's shader_versions (comma-separated, base first), then translate into <cache>/b-<chain version>/
DM_GAME=$G python3 dm/gfx/tools/shader_split.py $W/data/v0/RomFS,$W/data/$VER/RomFS $W/data/v0/ExeFS/main,$W/data/$VER/ExeFS/main $W/shader/pkg dm/games/$G/shader_map.tsv dm/games/$G/shader_facts.tsv dm/games/$G/shader_pairs.tsv
DM_GAME=$G python3 dm/gfx/tools/dmsh_build.py $W/shader/pkg $W/build/cache/metal_shader_cache # also writes dm/games/$G/c1_full.tsv and cb_extent.tsv
# 4. the host, and the button-icon font for the Nintendo-extension shared font
make -C dm/host GAME=$G dmhost
python3 dm/tools/make_ext_font.py $W/build/font/nintendo_ext.ttf
# 5. run on the Mac
DM_ROOT=$W DM_ROMFS=$W/data/$VER/RomFS DM_SAVE=$W/run/save DM_TEMP=$W/run/temp DM_MSL_DIR=$W/build/cache/metal_shader_cache DM_FONT_DIR=$W/build/font dm/host/dmhost $W/build/patched_$VER
# 6. iOS shader libraries (about 15 min for Mario), then the IPA (default <NSP folder>/<the profile's ipa>)
IOSOUT=$W/build/ios/dmsh_b DM_GAME=$G python3 dm/gfx/tools/dmsh_build.py $W/shader/pkg $W/build/cache/metal_shader_cache
GAME=$G W=$W dm/ios/tools/build_ipa.sh [out.ipa]
Notes on these steps:
dmsh_build.pystops when a tool differs fromdm/gfx/tools/dmsh_chain.lock. With another Xcode or spirv-cross, rundm/gfx/tools/own_compilers.pywith the same arguments and environment instead: it builds with the local compilers, and only the translator must match.dmsh_build.pycompiles each shader's Metal source through a cache keyed by that source, the compile flags and the compiler (dm/gfx/tools/dmsh_mtlcache.py, in<cache>/cas/metallib/). A chain change that leaves a shader's MSL unchanged, such as a translator fix, costs that shader no Metal compile. Before building, the cache takes the libraries of the earlierb-*/folders beside its output (and besideIOSOUT), and ofDMSH_PREFILL=<Mac root>[=<iOS root>](several separated by:). It takes a folder only when its recorded flags are the current ones and a fresh compile of a few of its shaders reproduces their libraries byte for byte. The summary lineMetal cache:shows reused, compiled and filled counts per target.build_ipa.shtakes the rewritten modules from$W/build/patched_<package>and the RomFS from$W/data/<package>/RomFS;PATCHEDandROMFSoverride them. In unsigned IPA mode,pack_ipa.pypacks the shader libraries, RomFS and add-on content beside the app Xcode built, asDmIOS.app/msl/b-<chain version>,romfsandaoc. Prepared Xcode projects instead sync these resources into the app before signing.- It also bundles a standard font (macOS's Arial Unicode,
STD_FONT=overrides it) and the generatednintendo_ext.ttf. - The icon is the
icon.jpgthatnx_extractsaves next toRomFS/;DM_ICON=points at another image. DM_ENV_FILE=bakes environment switches into the app, andDM_DISPLAY_NAME=renames it.DM_LANG=<code>is the game language (DMLanguageinInfo.plist, defaulten-US;main.msetsDM_LANGfrom it unlessDocuments/env.txtor the baked env file sets it).DM_SHOW_FPS=1turns the frame-rate counter on (DMShowFPSinInfo.plist, whichfront_ios.mreads;DM_SHOW_FPSinDocuments/env.txtoverrides it). It counts the frames put on screen each second (mtl_present_count), so frames the display link merges away are not counted.- The unsigned build pins its build product and intermediate directories inside
$W/build/ios/pkg/dd, independently of Xcode's Locations preference. - hactool cannot read every update (Mario 1.2.1's patch NCA gives "Invalid BKTR layout!"), which is why
nx_extractuses LibHac.
Prewarm lists
Metal compiles each combination of shaders and pipeline settings for the GPU the first time it is used. Each takes tens of milliseconds, so a burst of them on entering a level is a visible freeze. The host records what it had to compile and rebuilds those pipelines on a background queue at the next launch:
pso_user.tsvsits next to the save directory:Documents/pso_user.tsvon iOS, the parent ofDM_SAVEon the Mac (DM_PSO_USER=pathoverrides it). It is appended while playing, one line per item built on the spot: a shader (S), a render pipeline (R, the hex ofpso_skeyindm/gfx/nvn/nvn_pso_skey.h) or a compute pipeline (C).dm/games/<id>/pso_manifest.tsvis the seed list bundled with the app, prewarmed beforepso_user.tsv.
The lines name shaders by content key and hold no compiled code and nothing personal, so a list recorded on one device works on any other. To seed the bundled list:
- Record on the Mac with prewarm off, so everything is built on demand:
DM_PSO_MANIFEST=off DM_PSO_RECORD=<file>, playing the scenes players reach first. Engage's list covers boot, the opening movie, the title, a save load and the prologue battle. - Append the recorded lines below the header comments, in the recorded order. Duplicates are dropped at load, and so are shaders the shader map does not have (a list of another version of the game: the bundled list packed with a version the profile was not brought up on, or a device's
pso_user.tsvkept across a game update), which theprewarm doneline counts. - Rebuild the IPA.
Record on the game version you ship. At launch the log names each list it read and its line count (pipeline prewarm list …: N lines), or says that pso_user.tsv does not exist yet.
During rendering, a pipeline miss compiles and caches the demanded pipeline before scanning subsequent draws for speculative work. The scan admits at most two new jobs, with at most two speculative compilations outstanding; completed predictions remain available by exact key. This limit does not apply to the startup warmer. t_pso_pending checks scheduling and ownership with a gated compiler; it does not establish that the two-job budget is optimal on every device or remove the cost of compiling an unseen pipeline.
Pipeline bases and the pipeline archive
Concurrent requests for the same base share its in-progress build and result; unrelated bases can compile in parallel. Rasterizing pipelines without a fragment function use the MTLDevice path so depth-only draws keep their depth writes: Metal 4 requires a fragment function when rasterization is enabled. Transform-feedback pipelines, whose rasterization is disabled, can still use Metal 4.
Render pipelines are built through Metal 4's MTL4Compiler (pso_build in dm/gfx/nvn/nvn_mtl.m). pso_desc still describes each pipeline; from it the host builds a base whose colour attachments (format, blending, write mask) are left unspecialized, once per base, and makes every pipeline from its base by specialization, which takes a few microseconds instead of the 3–5 ms of a pipeline build. Metal 4 pipelines name no depth or stencil format, so one base serves every depth/stencil attachment. Breath of the Wild's 4,829 recorded pipeline keys came from 1,818 bases; 2,845 of the keys differed only in the blend factors of targets whose blending was off, which the key no longer holds (pso_key_norm).
The bases are kept in a per-game pipeline archive in pso_archive/ beside pso_user.tsv. Each file holds what one flush captured (NNNNN.mtl4a). A flush runs after the prewarm and 10 s after a burst of new bases. At start the host opens every file, and a base found in one is taken from it directly, in about 0.06 ms where building it takes 10–80 ms. Only bases no file holds are compiled, so each file holds new bases only. The serializer must capture binaries only: with descriptor capture as well, it writes no archive and reports no error. A flush and a compile must not overlap (a compile that finished during a flush crashed in Metal), so compiles hold a read lock and a flush the write lock (g_arch_rw).
Every start adds a file or more: Breath of the Wild wrote 5–7 per start, and each file a base is not in costs a lookup about 0.06 ms. With more than 8 files (PSO_ARCH_COMPACT) the archive is compacted: the bases the prewarm takes from the old files are taken again through the compiler, with those files as its lookup archives, which captures them; the flush after the prewarm writes them into one file, and the old files are removed (arch_compact_finish). Bases the prewarm did not take (no pso_user.tsv line names them) go with the old files and are built again when drawn. The log says pipeline archive compacted: N files gave M bases ….
The log shows pipeline archive …: N files, … MB, opened in … ms at start, pipeline archive: N new pipeline bases written to … after each flush, and after the prewarm a line counting the bases compiled, taken from the archive and reused. DM_PSO_ARCHIVE=off turns the archive off; DM_PSO_ARCHIVE=<folder> puts it elsewhere. DM_PSO_MTL4=0 builds every pipeline through the MTLDevice as before, with no bases and no archive. The Metal 4 path is on by default on the Mac only. On iOS it stays off unless DM_PSO_MTL4=1: the archive's memory is not measured against an iPhone's limit yet, and an archive cannot be made offline for an iPhone, because metal-tt rejects the pipeline script the Metal 4 serializer writes. iOS is a later step. Pipelines whose functions have no Metal 4 descriptor (the diagnostic DM_NVN_HILITE and DM_NVN_MAGSCAN overrides) always take the MTLDevice path.
During compaction, timed flushes defer while prewarm runs. Once it finishes, one archive-queue task flushes the recaptured bases and removes the old files. The pending count and serializer capture are protected by the same compile/flush lock, so a flush consumes the count for the bases it writes.
Power
The game's own work and its frame rate stay as they are; the host avoids work of its own that the Switch never did. Every 300 frames the log has a power line (CPU energy in watts and per frame, CPU time, wakeups, thermal state, the threads that yield most) and a residency set line; 05 explains them.
-
Yield loops. On the Switch, a yield with nothing else to run is free; a guest loop that polls the clock or a flag and yields is a busy spin on the host. Three Houses waited out each frame this way (228,000 yields a frame on an iPhone) until a per-build cut made its frame limiter sleep (see its notes). After 2 ms of consecutive yields with no other SVC, each yield sleeps an eighth of the wait so far, from 10 to 250 µs (
DM_YIELD_BACKOFF=0turns it off,=<cap µs>[:<start µs>]sets the cap and the start). Three Houses on the Mac, before that cut: CPU time 78% → 19% of a core, same frame times. The back-off first started after 200 µs, which also caught Tears of the Kingdom's main thread waiting for its workers on the frame's critical path: frames ran past 33 ms and the game dropped itself to 20 fps (9 times in 300 s on the Mac; on the iPhone the threads also stayed on efficiency cores). From 2 ms on it never backs off there. Not every yield loop waits, though: some yield between steps of work. A yield whose caller's callee-saved registers (x19–x28, which the SDK'sYieldThreadleaves as the loop had them) differ from the last yield's counts as progress: it returns at once with no host call, as a yield with no other thread to run does on the Switch, and the run starts again. A poll keeps its pointers and deadline in those registers; a loop that works moves its counters. Animal Crossing: New Horizons'SaveProcbuilds a new island in grid walks that yield once per cell: with the back-off the screen stayed black for 77 s after the opening movie, now about 10 s. Breath of the Wild'sPlacementMgrandRadarMgryield once per item of their batches (the back-off stretched a 94,263-item radar batch to about 30 s); outdoors on the Mac, CPU energy went from 0.35–0.74 to 0.21–0.34 W at 60 fps, CPU from 76–83% to 96–106% of a core, and the frame interval's p99 from 19 to 20–21 ms. -
Spin loops. A guest loop that polls memory with no SVC at all spins on a host core. On the Switch the spin fills only its own core, and a higher-priority thread pinned there preempts it. Paper Mario: The Thousand-Year Door's three job workers (
main+0xa8e680) poll the job list this way while a batch is pending; in Rogueport on an iPhone they took 94% of a core each and the game ran at 24 fps. The rewriter finds such loops in every module (patch_nso.py, "Spin waits"). A loop counts only when it provably cannot leave by itself:- every path from its head back to it runs only loads, register and flag arithmetic, direct branches and barriers (no store, call, SVC, system register or event wait);
- nothing live at the head is written on the way (no counter, cursor or delay);
- an exclusive add that every path back undoes on the same address is allowed (a worker that registers with a job, sees it finished and backs out);
- so is a spin lock taken and released on the same address on every path back (an exclusive test-and-set retried until it stores, then an exclusive compare-and-clear or a store-release of zero), a test loop on a word inside the loop (load, compare with a constant, branch back to the load), and up to four stores whose value and address registers the loop never writes. Each such store writes a fixed value to a fixed address, so the words it writes can only hold their first value or a stored one: a loop like that changes its own state for a few hundred passes at most (a fraction of the gate's 2 ms of polling), then its passes repeat and only another thread can end it (a loop with such stores must also load something). A store of a value the pass changes ends its path, and the search runs again without it: a poll that takes its item once the wait is over stores the new cursor there;
Such a pass leaves memory and registers as they were, so only another thread can end the loop, and a pause there changes only timing. The loop is copied into the patch area. Its original back-edges jump to the copy, so the first pass costs nothing, and the copy's back-edges go through a spin gate (
dm_spin_wait). A wait polls on through the gate for its first 2 ms and then sleeps as a run of yields does (DM_SPIN_POLL,DM_YIELD_BACKOFF);DM_SPIN=0never sleeps there. The gate alone slows the poll down, and on the Mac that was the whole gain: most waits are handoffs between workers, and asched_yieldon every pass raised frame p95 by 2–5 ms there.Breath of the Wild's job queue (
main+0xef62dc) needed the lock rule. The first item of some of its jobs is for the thread of core 0, and the threads of the other cores wait until it is taken (the queue's flag at +0x98, its cursor at +0x94), each taking and releasing the queue's lock (+0x1c) on every pass. The two workers (cores 1 and 2) polled at full speed, so the lock went back and forth between their cores, and the main thread (core 0), whose item it was, lost nearly every exclusive store to them: in 40 s of a Time Profiler trace it never once held the lock while the workers held it about 750 times each, and the three spun for 30–210 ms at a time, the CPU hitches in its notes below. Through the gate the workers leave the lock a moment between passes and the main thread takes its item at once. Zora's Domain on an M2 Max at 120 Hz, the same host, 42 s of steady play per run, three runs each, before → after: standing still, frames with more than 30 ms of CPU work 2.8 / 6.3 / 3.5 → 0 per 30 s, the frame's work p99 20–21 → 8.3–8.4 ms, frames late on screen 2.8 / 5.6 / 2.8 → 2.1 / 0.7 / 2.1 per 30 s (the rest are 120 Hz refresh slips); turning the camera, 11.9 / 13.3 / 6.3 → 0 CPU hitches, p99 21–25 → 9–15 ms, late frames 12.6 / 13.3 / 7.7 → 1.4 each. The search keeps every loop it found before and adds: Breath of the Wild'smain308 → 310 (this one, and a state setter atmain+0xf82230that stores its state and returns on the second pass unless another thread changed it), itssubsdk0one loop wider (0x27804, the movie library's thread loop, 6 → 23 instructions over the same back-edge and two more); Mario Kart 8 Deluxe'smain25 → 26 (a state setter like Breath of the Wild's,main+0x715df8) and itssubsdk0one loop wider (the same movie library loop,0x24118). Of the 206 modules on the game drives (108 NSOs and Super Smash Bros. Ultimate's 98 NROs), 175 rewrite byte for byte as before (Hades, Super Smash Bros. Ultimate and Super Mario Odyssey among them) and 7 fail under both rewriters (everyrtld, New Super Mario Bros. U Deluxe); the other 24 take new loops, all through the store rule alone (only Breath of the Wild's job queue has a lock): Monster Hunter Rise'smain90 → 166 (72 of them one inlined countdown,ldr w9,[x21,#0x24]; subs w9,w9,#1; b.ge; str w22,[x21,#0x24]; ldr; cbz, whose reset makes the next pass leave), BALL x PIT 87 → 94, Animal Crossing: New Horizons 162 → 166, Balatro 9 → 12, Octopath Traveler 0 75 → 78, Pikmin 4 80 → 83, Bayonetta 3 35 → 37, and one more each in Big Brain Academy, Metroid Dread, Pokémon Legends: Z-A, Stardew Valley, Super Mario Bros. Wonder (both builds) and Tears of the Kingdom; the movie library loop is wider in thesubsdk0of Animal Crossing, BALL x PIT, Monster Train 2 and Octopath Traveler 0 too, and one loop is wider in themainof AI: THE SOMNIUM FILES and of Advance Wars 1+2. No loop of the old search is lost or loses a back-edge. Each distinct shape was read in its disassembly: one-pass resets and state setters (the gate is reached only when another thread changes the word), parser states that leave on the next pass (Octopath Traveler 0main+0x4ebfa54, Balatromain+0x1f4600), and real flag waits (Big Brain Academymain+0x1734e0, the movie library). Monster Hunter Rise, the module with the most new loops, runs alike on the Mac with both rewrites from a fresh save (180 s headless from the boot each, the same host: 60 fps, frame p95 about 17.5 ms, the same red lines, no hang). Searching paths through stores costs time: Breath of the Wild'smaintakes 26 s of CPU to rewrite instead of 19, all 206 modules about 4,100 s instead of 3,250.Results, on the Rogueport route on the Mac, side by side with the committed build under the same load: frame p95 29–37 ms → 18–22 ms, CPU 93–129% → 38–57% of a core, the workers 576–885 → 65–94 ms/s, CPU energy 22–32 → 11–17 mJ per frame. Loops found: TTYD 17 in
main, 5 insdk, 8 insubsdk0; Wonder'smain175, Breath of the Wild's 308, Tears of the Kingdom's 843. In Tears of the Kingdom, 701 of them are one inlined atomic decrement (a compare-and-swap retried after another thread changed the count), which yields only on a second retry in a row. The x18 scan now decodes in blocks without details, so the rewrite takes less time than before even with the search: TTYDmain20.5 → 11.7 s of CPU, Tears of the Kingdom's 101.5 → 64.2 s, Monster Hunter Rise's 507 → 342 s. -
Waits block instead of polling: the in-process audio renderer, upload batches,
nvnQueueFinish. A signalled event wakes only the threads waiting on it; leaving an SVC takes the global lock only while a pause (IL2CPP's garbage collector) is pending. -
Real-time policy. The thread that acquires the window texture declares a Mach time-constraint policy, which tells the performance controller the frame deadline.
DM_RT=0leaves it on its QoS, for measuring. -
Frame-step probes. A game that waits out a longer hold (
nvn_fstep.h, 30 fps after late frames) gets parked on the efficiency cores even though the declared period stays at 60 fps. Tears of the Kingdom on an iPhone ran 6% on performance cores at 1.6 GHz, thermal 0, where its frames never fit 60 again. While under 30% of the process's CPU time runs on performance cores, in two windows of 30 presents in a row (thermal state nominal or fair, Low Power Mode off), the frame step therefore tries 60 again after its hold-off even when the cost does not fit. The first 30 frames of such a probe may be late, so the controller has time to raise the clock. A probe whose run at 60 then ends within 8 s counts as failed, so the next one waits twice as long (1 → 8 s). The GPU clock follows the hold too: Breath of the Wild on an iPhone went from 14.4 to about 25 ms of GPU time a frame at 30 fps with the same 4,900 draws, its CPU still on the performance cores, and never tried 60 again. A GPU time at the longer hold of at least 1.3 times its time at 60 fps that alone does not fit 60 (nvn_fstep_gpu_slowed; the reference is not updated during a probe) also allows probes, under the same gates. A hitch starts probes too. A step up after 8 s or more at 60 fps keeps what that run's frames cost (frames on time only), and a cost at the longer hold of at most 1.1 times it means the scene costs what it did (nvn_fstep_same; measured, so no thermal gate). The fit trial alone did not bring such a game back: the iPhone lowers its GPU clock until the work fills most of the frame, so a GPU-bound game's cost sits near the fit line (0.85 of a vsync) at either hold. Breath of the Wild, stepped up by pipeline compiles after a cutscene, took 12.8–15.2 ms of GPU time a frame at 30 fps (p50), where 60 had held for minutes, and stayed at 30 for 36 and 46 s until the player switched out and back (the same 4,900 draws took 5.8 ms right after a trip to the background and 13.5 ms half a minute later). A run under 8 s keeps no cost, so a device that steps up every few seconds stays at 30 rather than flap. Log:frame step: hold 2 → 1 … (probe, CPU N% on performance cores),(probe, GPU 1.70× its time at hold 1)or(probe, cost 1.04× the cost that held hold 1),the probe held / failed, and on a step uphold 1 held at 0.86.DM_TEST_PRESENT_LAG=1,10;1200,30,3imitates such a hitch on the Mac. -
Thread classes. Every guest thread runs at QoS user-interactive, whatever its Switch priority.
DM_QOS_MAP=1(off by default, for a phone A/B) maps priority ≤ 44 to user-interactive, 45–59 to user-initiated and the rest to utility (DM_QOS_EDGES=44,59moves the edges).SetThreadPrioritymoves a thread's class too, and a thread waiting on a lock lends its class to the owner until the lock is handed over. Host helper threads then set their own class (audio notify, silent sink and vsync user-interactive, movie decode user-initiated) instead of inheriting it from the guest thread that made them. -
Replay CPU. Memory-pool addresses are cached per thread; per-encoder draw state is sent only when it changes; texture IDs of GPU-only textures stay cached when the game writes other memory; upload staging blocks are reused (Breath of the Wild used to allocate, make resident and free a 32 MB block for each re-upload of a small CPU-written texture, about 3 GB/s) and the residency set is committed only when it changed.
-
Vertex bindings. Normal and transform-feedback draws reuse resolved stream addresses until the guest address or memory-pool generation changes. The vertex argument table skips unchanged addresses, including slot 30's zero buffer; converted streams and shader-argument writes update the same table cache.
DM_NVN_VBUF_CACHE=0restores repeated lookups and table writes for an A/B run. Every 300 frames,vertex bindingsreports per-frame lookups/reuse and table writes/skips (the table counts include vertex shader arguments). Compare the existing submit CPU/instruction and onscreen interval logs as well: fewer binding calls alone do not establish a frame-rate gain.make -C dm/gfx/tests GAME=<id> t_vbufbuilds the GPU-readback regression; run it withMTL_DEBUG_LAYER=1, both with the cache enabled and withDM_NVN_VBUF_CACHE=0. -
GPU. An inline uniform update inside a render pass no longer ends the pass (Metroid Dread ended about 246 passes a frame this way, each one storing and reloading its attachments): the update lands when the pass ends, and the draws after it read a copy of the updated data (
DM_NVN_UB_DEFER=0restores the old behaviour). When the update covers only part of what a draw's shader reads of the block, the rest of the copy comes from memory and the other inline updates queued in the submission, unless a copy, fill, storage write or an earlier submission still running may have written it (DM_NVN_UB_PARTIAL=0ends the pass instead;DM_NVN_UBP_CHECK=1checks every such copy against the GPU's memory). Attachment loads and stores per frame on the Mac, old → new: Fire Emblem Engage's battles about 1,850 → 360 MB (88 → 38 passes), Tears of the Kingdom 1,100 → 700 MB, Link's Awakening and 13 Sentinels about 15% less, Super Mario 3D World 5%; Breath of the Wild 386 → 158 passes; Mario Wonder and Metroid Dread unchanged (their updates already covered what the draws read). Clears with no draw after them no longer open a pass of their own when the game sets other targets: the next pass that attaches the same target clears it as its load action (DM_NVN_LAZY_CLEAR=0restores the old behaviour). A format-changing texture copy that covers the whole destination level does not load it first.DM_NVN_BARRIER=gamelets a pass's vertex work overlap the previous pass (04 section 7); it is off by default, as the games issue their own barriers before most passes anyway.
Game notes
Super Mario Bros. Wonder (010015100B514000)
- Versions: v0 (1.0.0) and 1.2.1 (v327680). With the update layered over v0, unchanged files become APFS clones, so the merged RomFS adds only about 114 MB to the 3.5 GB base.
- Shaders: 12,811 in v0 and 16,268 across both versions; the tables hold both.
- 1.2.1: it is a new
mainbuild (SDK 16.2.12), whose resource-heap cut (+16 KB slack) was carried over withdm/tools/nso_fnmap.py. It also makes new SDK calls, which the host answers:nn::nfpfinds no amiibo,pl::IsRunningOnOunceis false, the shop call is a no-op, andaccount::PopOpenUsersreturns none. - Mac input:
DM_KEYS="12:A,14:A,20:A,26:A,32:A"reaches the opening course.
Fire Emblem Engage (0100A6301214E000)
- Engine: Unity 2020.3 IL2CPP, SDK 12.3.7. The shaders are read out of Unity bundles (
unityfs.py). - Memory: a 5 GB guest block, the total proven on a phone, with a 2.5 GB application pool. A per-build cut shrinks the SDK's address-region node area to 62 MB.
- Audio: Wwise drives AudioOut directly.
- Movies: VP9, decoded on the host.
- 60 fps: the Switch version runs at 30; one patch to its code switches it to the 60 fps timing the game already has, so nothing runs too fast.
The Legend of Zelda: Breath of the Wild (01007EF00011E000)
- 60 fps at normal speed (30 on the Switch). The game steps its time by the frame interval its framework picks (
ksys::VFR: interval / 2 of a 30 fps tick). With the profile'sfps_target(60) the host picks it instead, in whole vsyncs (FRAME_STEPin the profile'spatches.py,hle_nvn.cfstep_tick,nvn_fstep.h, the same frame step as Tears of the Kingdom's): a stub at the framework'ssetWaitVsyncIntervalhands the game the host's step once a frame, and the host keeps each frame on screen that many vsyncs.- It holds 1 (60 fps). Three late frames in a row (the frame or its GPU work more than 1.7 ms over) step up to the cost, 2 (30 fps) as a rule: no 40 fps on 120 Hz screens. 30 frames that would fit the shorter hold try it again, and each failed try doubles the wait before the next (1 → 8 s). After a hitch, a cost at 30 like the one that held 60 tries it too (probes, Power).
- Real time lost to a late frame or a hitch is paid back one vsync per frame (at most 15; the rest of a long load is dropped), so game time = real time. The log line
frame step: game time 30.00 s in 30.00 s (1.000 of real time)checks it every 1,800 presents, with the frames held 1 / 2 / 3 vsyncs, late frames, paid-back vsyncs and hold changes. - Scenes that fix the game's step themselves (the VFR's override: 2 at
main+0xc3b588/+0xc5ebf8, 1 atmain+0xc609d8) keep it, and the host holds frames that long (log linethe game fixes its own step; not reached on the Mac yet). - Mac (M4 Pro), from the title into the save's scene: 1.000 of real time at 60; with 12 ms added to every frame (
DM_TEST_PRESENT_LAG=1,12) it drops to 30 and stays at 1.000.DM_FPS_TARGET=30holds 30 (saves power).
- CPU hitches (Mac): in steady play, frames with 30–210 ms of CPU work came every 1–10 s (Zora's Domain, M2 Max), the main thread and the two workers spinning on a job queue's lock that the main thread could not get. Since the spin wait rule takes that loop they are gone (Power, Spin loops).
- The game's own interval controller (at
DM_FPS_TARGET=0, and the interval the framework keeps while the VFR override holds): patches to it lift its floor from 2 to 1. The game's time step follows its frame interval, so the speed is right at a steady 60, 30 or 20. The game picks the interval from the frame's CPU time and its GPU time, which it reads from two GPU timestamps around the frame; the host gives each timestamp the GPU time of the work around it (profile keygpu_timestamps, which its dynamic resolution needs too). Before that it saw only the main thread's CPU time (about 3 ms a frame in the first shrine) and stayed at 60 in slow motion: at 40 fps, about two thirds of the normal speed (an iPhone 16 Pro Max in handheld mode, GitHub issue #9; 04). Under load it still ran at 0.93–0.98 of real time. - Outdoors on the Mac (M4 Pro, the Great Plateau): about 165 render passes a frame instead of 386 and GPU p50 about 6.5 ms instead of 9.3 since a partly updated uniform block no longer ends the pass (the game updates 64 bytes of a 64 KB block before each draw group), and the G-buffer clears ride on its first pass (04). The frame still loads and stores about 800 MB of attachments, most of it where compute dispatches of the GPU particles split passes.
- Render scale:
DM_RENDER_SCALE=0.5–1.0(also fromDocuments/env.txt) fixes the 3D views at that fraction of 1600×900 docked (1280×720 handheld) and turns the game's own dynamic resolution off; 1.0, the default, keeps it. A stub sets the scale where the game applies its own (main+0x1266880), so every scene pass, the shader's resolution ratio and the LOD check follow it: at 0.5 the 1600×900 passes become 800×450 and the 800×450, 400×225, 200×112 and 100×56 chains halve too, while the shadow maps and the 1920×1080 output keep their size and the HUD stays sharp. It holds in handheld mode too (960×540 at 0.75). On the Mac (M4 Pro) the GPU time does not change (about 13 ms in the first shrine at 1.0 and at 0.5): a frame has about 650 passes, most of them tiny, whose fixed cost outweighs their pixels there. Whether it helps an iPhone, whose GPU loads and stores each pass's attachments, is what the setting is for. - The world-load crash (about 1 run in 3 on the Mac) came from the host's command record sizes. About 3 s after Start, a task worker's
operator new(main+0x155a79c) got null and the game wrote through it (main+0x155a7a4). Each model file gets a heap of its resource size table entry + 0x1008; the file goes in 4 KB aligned, and the model's setup (main+0x155a2ac) takes the rest as a frame heap. Part of that setup is GPU memory for the model's prerecorded commands, which the game sizes by recording each mesh's draw and each shape's state for each render pass into a scratch command buffer and adding upnvnCommandBufferGetCommandMemoryUsed, so the host's own record sizes count against the table. Draw records had grown to carry the indirect and multi-draw fields (0x38 → 0x50 per recorded draw on 2026-10-18 and 10-20), and every recording began with an 8-byte BEGIN record.Obj_TreeConiferous,Obj_TreeBroadleaf_BandFldObj_HyliaBridge_A-02then needed one 4 KB page more than their entries allow, which fit only when the file's alignment pad happened to be small (about 15% of loads each failed). Plain draws now leave those fields out and recordings have no BEGIN (0x30 per recorded draw,nvn_obj.hdraw_rec): all 510 models of the world load fit whatever the pad. How it was measured: 03. - Memory: about 2.8 GB in the first shrine in handheld mode, idle or walking (Mac measurement), against the 3.4 GB an 8 GB iPhone gives the app.
- The game waits for each frame's GPU work before submitting the next, so the host hands its translated commands to the GPU in segments (04).
- Sky (GitHub issue #52): outdoors the sky was brownish grey and the distant haze brown, on the Mac and the iPhone. The game copies its precomputed sky scattering tables (
System/KSys/sky.skybin) into memory pools with the CPU and flushes them; the host made the 3D table a GPU texture of zeros. Every game now fills a texture from its storage at first use when the game flushed that storage before then (04); the sky is blue again. - HUD prompt shadows and glows, stamina wheel, System menu background: the game rebuilds each layout capture (a blurred mask of a prompt bar, the menu's background) on the same storage with its own channel mapping and keeps the new texture only if
nvnTextureComparecalls the two different; the host's comparison ignored the mapping, so captures read alpha 1 (hard-edged dark boxes, cream cloud rectangles, a black menu background). It compares the mapping now (04).
The Legend of Zelda: Link's Awakening (01006BB00C6F0000)
- Engine: Grezzo's engine on NintendoWare, SDK 8.2.0 (the oldest so far), NVN 53. Modules main + subsdk0 (GLSLC, NVN's runtime shader compiler) + subsdk1 (movie) + sdk; 9,698 shaders from the bfsha archives, effects and layouts.
- Window origin: the only game so far that keeps NVN's default lower-left (OpenGL) window origin; the host flips its viewports and winding and the window image at present (04). Before that, text the game renders into textures came out upside down.
- Movies: VP9 with a 5.1 Vorbis sound track; the host decodes the sound with stb_vorbis, which needed a local fix for type-2 residues with more than two channels (README). Before that, the opening and title movies sounded distorted and gurgling, with pops.
- Lighting: tiled forward lighting whose tile lists a compute shader builds each frame; it needs the dispatch size in NVN's driver segment
c0(04). Before the host supplied it, lights piled into the bottom tile row and the doorway step flickered with white tiles. - Sound: SDK 8 numbers the audio renderer's commands differently (the output command is 0x10, not 0x13); the host recognises the output command by its size. Before that, music and sound effects were silent and only the movies had sound (03).
- Mac input: the title wants L+R:
DM_KEYS="f6000:L+R~0.4,f6300:A,f6500:A,f6700:Down,f6800:A"reaches a new file's name entry (a fresh save directory viaSV=). - Frame rate: gameplay runs at 60 like on the Switch; the game itself drops to 30 (present interval 2) for event cutscenes.
- Depth of field: outdoors the game blurs the top and bottom of the screen by depth. It sets NVN's near = 0 depth mode, which the shader translation now follows for every game (an entry point per mode, picked at run time); before that the whole screen was blurred (04).
- Sunlight: outdoors the sun's shadow test failed everywhere, so the scene was dim and nothing cast a shadow. The world position its shaders pass between stages has no w written, and the translation filled it with 0 instead of the hardware's 1; every game's shaders now get the hardware's default (04).
- Physics: at 30 fps the game steps physics twice per frame. Its completion task destroys itself after it runs, and on Apple devices that destructor could land after the next step had already rebuilt the task, so the physics workers crashed when a conversation or cutscene began. A per-build cut drops the destructor call (03).
Super Mario Odyssey (0100000000010000)
- Engine: Nintendo EPD's sead / agl with NintendoWare, SDK 7.3.2 (the oldest so far), update 1.4.1. Modules main + subsdk0 (movie) + sdk; 27,984 shaders (agl
sharcbandbfshainShaderData, particles inEffectData); its three geometry shaders (a thunder effect) do not translate. - Start-up services: it calls several system services no other game did, each answered by the host: the aborting
account::OpenPreselectedUserandGetLastOpenedUser, news delivery (bcat: the delivery storage does not mount, which the game treats as "nothing delivered"), HOME-button blocking, the parental 3D-mode setting,vi::SetLayerScalingMode,time::ToPosixTimeFromUtc, the debug pad, and the SDK 7 names of two audio-device calls (03). - Movies: MP4 / H.264 (the videos in its tutorial hint boxes, the kingdom travel videos), 41 of 44 High or Main profile with B-frames. The host decodes them with VideoToolbox; the SDK's demuxer passes the parameter sets as the
avccrecord (the MP4avcCbox), and pictures leave in presentation order through a small queue. - Sticks: the game reads all 16 entries of the controller history whatever count comes back; the host now fills every entry (before, only the first, and neither stick did anything in play while buttons and menus worked).
- Motion: it reads the six-axis history the same way (16 entries per sensor, the count ignored,
main+0x93fd84). Its motion analyzer (main+0x5d5590) turns the newest acceleration, seen against the controller's pose, into a swing value per hand and sets the swing flags when it exceeds 0.2. A still controller gives 0, and the host's shake (±3 G side to side) gives up to 1.5, so aShakesets the left, right and any-swing flags for its length. Before, the host reported no sensor and no motion move could happen (03). - Mac input: a fresh save wakes Mario in the Cap Kingdom with
DM_KEYS="f1000:A,f8200:RLeft~6,f9200:LUp~4": the "Look Around" hint waits for the right stick held about 6 s one way (left then right cancels out). Play starts at about frame 9,900, and the game saves there. From that save,f1000:A,f1300:Aresumes into play at about frame 1,500, andf1600:LUp~7walks straight across the fog to the hills by the bridge, where Cappy flees from Mario. - HUD: the coin counters are layout panes that take their alpha from single-channel render targets, as in Breath of the Wild; the profile lists the 26 layout shaders under
alpha_from_red(before, they were solid white cards). Since thenvnTextureComparefix the game's own capture mapping does that and the list no longer fires on the Mac (04). - Ground: a depth pre-pass followed by a less-or-equal G-buffer pass needs bit-identical positions from different vertex shaders; every shader is now compiled with
-fpreserve-invariance(before, the Cap Kingdom ground showed stale 32×32 blocks on every fourth frame) (04). - Fog: the Cap Kingdom ground fog parts around Mario through a small fluid simulation fed by point-sprite splats. Their vertex shader clamps the point size to the driver segment's
c0 + 0x610, which the host left at 0, so no splat was ever drawn and the fog never moved; the host now writes the largest point size there (04).
Hades (0100535012974000)
- Engine: Supergiant's engine on The Forge renderer, SDK 9.3.0, update 1.0.38233. It renders with Vulkan:
mainimports 100vk*functions from subsdk0 (the Vulkan loader, over NVIDIA's driver in subsdk1). The host implements them on Metal itself (dm/gfx/vk/hle_vk.m), so only main and sdk are loaded. - Vulkan layer: one Tegra X1-like device (Vulkan 1.1); device memory is a shared
MTLBuffermade on first use, images are their own textures; command buffers are recorded and replayed into one Metal command buffer per submit on the layer's own queue; swapchain images are shown through the NVN layer's present path once their frame's GPU work is done. Names the game asks for throughvkGet*ProcAddrare logged once each. - Shaders: 74 SPIR-V files in
Content/EffectsForgeSwitch.dm/gfx/tools/vk_shaders.py <RomFS> <shader dir> [--ios <dir>]translates them with SPIRV-Cross into<dir>/vk/: one library, and a table of the Metal slot each (set, binding) moved to. The game hands the same words tovkCreateShaderModule, so the host finds each shader by the hash of its SPIR-V. - Notification event: the game polls
oe::GetNotificationMessageEventevery frame; the SDK's own event was never created (the host'soe::Initializeis a no-op), so the host now hands out an event of its own, signalled while a notification is queued. - Online: its
EOS_*imports (Epic Online Services, an NRO it would load throughnn::ro) stay offline. - Mac input:
DM_KEYS="f800:A,f1000:A,f1200:A,f1500:A,f1900:A"goes through the title and the intro into Tartarus. - Packing: for a Vulkan game the NSP script runs
vk_shaders.py(Mac and iOS libraries in<id>_build/shader/vkandshader/ios/vk) in place of the NVN shader split and chain build.
Hades II (0100A00019DE0000)
- Engine: Supergiant Games' engine, SDK 19.3.9, base game v0 (the NACP's display version is 0.0.126424). Unlike Hades it renders with NVN, and its ExeFS holds only main and sdk. The NSP script's provisional profile booted it on the Mac with no host change: title, a new save, the opening and Erebus at 60 fps. It plays on an iPhone.
- Shaders: 88 BNSH files in
Shaders/NVN/bin(.vert/.frag; the.shaderfiles list each program's pair). They bind textures at fixed, sparse slots: 0, 1, 3, 6, 8, 11, 15, 21, 25, 26, 30, 31 and 34. Slot 34 (SceneFade.frag, c2 0x130) lies past the driver's 32 combined slots, inside the image table at 0x120; the game has no image bindings, and a bound texture there wins over an unbound image (nvn_mtl.m). - Online: its
EOS_*imports (Epic Online Services,nro/EOSSDK-Switch-Shipping.nro) stay offline, as in Hades:ro::RegisterModuleInfofails (red line), so the game never loads the NRO. SDK 19'snn::ro::LoadModuletakes one more bool (…mib); the host hooks that form andro::Finalizetoo, so neither reaches the SDK'sldr:roclient. - Memory: no system resource; the game asks for a 3.10 GB heap, which the 4 GB layout's heap region holds. main has an 82 MB bss, so the profile's
mem_usedis 108 MB. Resident memory levels off at about 3.4 GB in Erebus on the Mac. - Not a bug: two long lines through Melinoë. While the staff's Omega attack charges, two thin white lines run from her to the screen edges, one forward and one back. They are that weapon's aim lines (
WeaponStaffSwing5:AimLineCountOverride = 2,AimLineAngleOffsetOverride = 180, animationAuraAimLineNoMovementStaff). The sprite is a long arrow with white edges, a dark fill drawn emissive and its tip off-screen, and the game hides it while she moves. Traced withDM_NVN_CBDUMPvertex dumps andDM_PROBEon the aim-line update (main+0x39ade0); a scripted run that taps Y shows them often. - Mac input:
DM_KEYS="f700:A,f900:A,f1100:A,f1300:A,f1500:A,f1800:A,f2100:A,f2400:A,f2700:A"goes through Play, the first save slot and the opening into Erebus (control around frame 3000).
Hyrule Warriors: Age of Calamity (01002B00111A2000)
- Engine: Koei Tecmo's KTGL on NVN, SDK 9.6.0, update 1.3.0 (v327680), four DLC packages (one is only a licence). Modules main + subsdk0 (movie) + sdk.
- Files: everything sits in RDB archives (
asset/*.rdbindex;asset/*.rdb.bin*andasset/data/*.fileholdIDRKentries, zlib in 16 KB chunks) (01). - Shaders: 40,726 GLSLC outputs: KTGL's G2S containers and one
_DOKcontainer insystem.rdb.bin*, plus 117 inmain. Many have 0x850-byte controls and the compute ones no code header (04). The game builds 8,705 programs during its first frame (about 10 s on the Mac). - Heap: its three modules take 66.6 MB, more than the 64 MB the host reported as used, so
SetHeapSizefailed andnnMainreturned at once; the profile'smem_usedreports 72 MB. - Sound: the engine's 15 MB heap was too small on the host (renderer work buffer plus four players, three of them 5.1). The sound setup then gave up quietly, the first sound archive never loaded and the main thread waited forever; a per-build cut moves 4 MB from the 142 MB sound data heap to it (03). The multi-stream hardware Opus decoder is the SDK's software one.
- Graphics fixes it brought: compute programs get their own binding in the replay (draws after a dispatch were dropped, so the intro movie stayed black),
nvnTexturePoolRegisterImage, indirect draws (its GPU-driven effects are indirect quads, converted by a compute pass), and the movie planes (CPU-written, published withnvnMemoryPoolFlushMappedRange) throughtex_flush_upload. - 60 fps (30 on the Switch). The game steps a fixed amount per update, from three 30 fps values: a KIDS task on
KIDSTaskSchedulerthreads (main+0x166d980) waits until 1 / fps has passed since the last update and steps the game clock by a frame-rate object's 1/30 s (not by the time that passed); the battle steps 1 / the game's frame rate per update (a block at main+0x269e150, {30, interval 2, 30, 30.0}, read by the scene update's caller at main+0x8c3610 and its jobs); and the event scripts step by the KTGL device's frame rate, 59.94 / the present interval in its config (2). The first 60 fps cuts (interval 1, a 1/60 s wait) were checked only on cutscenes, which follow the device:DM_PROBEshowed the game clock at 2.00 × real time, and battles ran twice as fast on an iPhone. Fixing the clock alone was not enough: in a 3 s stick hold Link still covered twice the ground of the 30 fps original, and the enemy waves came early, until asampleprofile of a battle led to the frame-rate block. The game's updates are not held by the presents (the task kept 60 updates a second while the host held frames 3 vsyncs), so three frame-step stubs (FRAME_STEPin itspatches.py) run the updates at the cap: the task's fps and the frame-rate block become the cap, the object's step is scaled by its fps × vsyncs per update / 60, the device frame rate becomes 59.94 / vsyncs, and nnMain's device config gets interval 1 above a 30 fps cap. Every step stays per update, as on the Switch, so an update rate below the cap slows them all together. The host shows each frame as soon as it is ready (profilefps_hold off); the updates keep their speed whatever is shown. Profilefps_target 60;DM_FPS_TARGET=30runs updates and presents at 30, andDM_FPS_TARGET=0is the Switch original (interval 2, the stubs' original paths). On the Mac the forced 30 and the original put Link on the same spot after the same stick hold, and the opening scenes at frame 2N at 60 match the original's frame N (same subtitle, same poses). - Battles: vertex conversion over the draw's vertices (
vconv_range). The soldiers' Half2 and Short2 attributes feed integer shader inputs, which the replay converts on the CPU (vconv_bindinnvn_mtl.m). KTGL binds whole mesh pools of up to ~590,000 vertices, so converting the bound ranges came to 4.4 million vertices per submission in the first battle: 24 of the 32 ms the replay took per frame on the Mac, 26 fps at a 30 or a 60 fps target. The profile'svconv_rangeconverts only what each draw reads (219,000 vertices per submission, about twelve submissions a frame), and the half and 8 / 16-bit integer formats have conversion loops of their own. Keeping the conversions across frames did not pay: the soldiers' attributes change every frame.DM_NVN_VCONV_CHECK=1found no stale index range in 983,000 reuses. The visibility buffer ring was 4 deep, so the submit thread waited for the previous frame's GPU work; it now has as many buffers as command allocators (8). The battle runs at 58.6 fps on the Mac (M4 Pro; frame p50 16.9 ms, replay CPU 10 ms, GPU 11 ms). On an iPhone the GPU decides (14–16 ms per frame at p50 in the first battle, 40–60 ms once the phone reaches thermal state 2): the frame step holds 30 there. - Battles after the first cutscene: compute dispatches and texture views. An iPhone log (2026-10-21,
fps_hold off, thermal state 0) went from about 57 to 17 fps after the first battle's first cutscene: the same 1,000–1,400 draws a frame, but NVN calls from 82,000 to 287,000 a frame, GPU 15 → 51 ms and the replay 12 → 24 ms. The battle then runs more small compute dispatches (on the Mac about 280 → 900–1,500 a frame, each with a game barrier and about 17 texture binds), and two host costs grew with them: every dispatch had a compute encoder of its own with full queue barriers on both sides (about 10 µs of GPU each on the Mac), and KTGL registers a texture into the next ID of a ring before every bind, so the per-ID view was rebuilt for nearly every bind (half of the 8,000–28,000 registrations a frame made a new Metal texture view, 45% of the replay's time). Dispatches now share the copy encoder (about 1.3 µs each) and views are cached by texture serial (04). Mac runs side by side, after the cutscene: GPU 18.0 → 8.1 ms and frame 35 → 22 ms with the dispatch change; the view cache took the replay before the cutscene from 14.3 to 10.8 ms (2,036 views made in 15 minutes). - Light and decal lists, vertex formats: two general fixes for its last red lines. 16,438 shaders read KTGL's light and decal lists from buffer textures with the instruction Maxwell also uses for 1D textures. The translation read them as 2D and the host bound a dummy, so every list read 0. The lists are now read as rows of 16,384 texels from a linear 2D texture over the buffer ("row wrap"). The first battle is darker and more saturated than with the zeros. The CPU vertex conversion of the soldiers' Half2 / Short2 / RGBA8UI attributes was a quarter of the replay thread; it is replaced by shadow vertex inputs: 2,259 side libraries, 77 MB each for Mac and iOS. Mac battle at 60 fps against the CPU conversion: submit + present 10.0 → 5.7 ms, CPU 3.97 → 2.44 W (04, both general). A 2D texture bound where three vertex shaders sample 3D (slots 8 and 9,
texture type mismatch … declares slot 8 as Metal type 7) is still bound as is; it was hidden behind the earlier red line. - Other games' saves: it asks whether Breath of the Wild's (and
0100A2C01320E000's) save exists for bonus items; the host says no. - Mac input: with a save whose first-boot settings were made,
DM_KEYS="f300:A,f420:A,f540:A,f660:A,f780:A,f900:A,f1300:Plus,f1350:Plus"confirms the settings, skips the intro movie and plays the opening into the first battle, The Battle of Hyrule Field (about frame 4000).
Fire Emblem: Three Houses (010055D009F78000)
- Engine: Koei Tecmo's older KTGL (the game is co-developed with Intelligent Systems), SDK 6.5.0 (the oldest so far), update 1.2.0. Modules main + subsdk0 (GLSLC, NVN's runtime shader compiler) + subsdk1 (movie) + sdk. RomFS: the LINKDATA archive (
DATA0.binis the index,DATA1.binholds 6.9 GB), loose overrides inpatch1–patch4, WebM movies and KTSL sound banks; compressed entries and*.gzfiles are Koei Tecmo's chunked zlib (01). - Shaders: 1,364.
shader_split.pyreads the SPKG packages (G2S containers, as in Age of Calamity) inDATA1.binandpatch*/nx/shader, a_SPK4100post-effect package, and the BNSH and GLSLC outputs inmain. SDK 6's GLSLC writes 0x848 / 0x850-byte control sections and compute code with no header, which the reader used to drop (04). - Shaders compiled at run time: KTGL compiles 9 built-in GLSL shaders at boot with the game's own GLSLC (2D primitives, text, the loading quill). No game file holds their output, so they are captured on the Mac. The game runs with
DM_GLSLC_CAPTURE=<version dir>/glslc_capture(hle_glslc.c), andshader_split.pyreads that folder. The outputs are identical on every run. The NSP script does this by itself in itsGLSLC capturestage, so the NSPs and prod.keys are enough. The stage builds the Mac host into<id>_build/tools/and boots the game until the 9 files are in<id>_build/extract/v327680/glslc_capture, which takes about 10 s in all. Without the folder the split tables would differ from the profile, and the script would stop at its table check. By hand, run the game for a few seconds with that variable (step 5 of the build by hand). - Host changes it needed (03, 04):
- it submits on two NVN queues from two threads (submissions are serialized now);
- its command buffer records into one 16 MB ring chunk and frees the ring by
GetCommandMemoryUsed, which now counts from the lastAddCommandMemoryas NVN does (frames stopped after about 15 s); - its two texture pools and two sampler pools use the same IDs, now kept apart (menus showed plain bars or only the cursor);
- SDK 6's
account::OpenPreselectedUserreturns a Result (the save never mounted and the first loading screen never ended); - SDK 6's audio command header has its own layout (no sound);
- its character shaders read Half2 UVs through integer-declared inputs, now converted to float32 bits at replay (black silhouettes, then flat tan characters);
- depth clamp is replayed.
- Fixed: the ground under close-up battle cameras showed as flat blue. The terrain's signed 16-bit heights were read zero-extended, so the vertices with negative heights went out of view (04, Three Houses).
- Fixed: the first loading screen took 78 s on the iPhone. The loader calls
nvnQueueFinishafter each resource it uploads, and that call waited for the display to take every queued frame; it now waits only for the GPU (04, Three Houses). - Light and decal lists read since row wrap (04, general): 321 shaders read them through the instruction Maxwell also uses for 1D textures, and they used to read 0. Torches and burning houses now light the prologue battle map and the close-ups, and its Half2 / RGBA16I vertex attributes go through shadow vertex inputs instead of the CPU conversion. To confirm: 64-pixel rectangles in the ground where decals land, which skipping the decal pass removed; they did not appear on the Mac prologue route in either build.
- Mac input to a first fight: from a save right before the prologue battle, with
DM_AOC_DIR=<NSP folder>/010055D009F78000_build/aoc(or a "DLC deleted" dialog blocks loading),DM_KEYSwith A every 90 frames fromf720tof2610, thenf2700:B,f2800:B,f2900:A,f3000:Up,f3060:Up,f3120:Up,f3180:Up,f3240:A,f3340:A,f3440:A,f3540:A,f3640:A,f3740:Acloses the movement tutorial, moves Byleth up to the thief and attacks: the combat close-up runs at frames 3,400–3,700. - Memory: about 3.5 GB resident and 2.8 GB of Metal allocations on the Mac; the game itself makes a 1.5 GB GPU-only memory pool.
- 60 fps: the Switch version shows 30, but KTGL already updates the game 60 times a second on its main thread, each update a fixed 1/60 s step; the render thread draws the latest update and presents it with the interval of the render mode (2 for the docked 1080p and handheld 720p modes; the engine also has a 720p and a 1080p mode at interval 1 that nothing selects). Per-build cuts on 1.2.0
mainset every mode's interval to 1, so every update is shown and the game runs at the same speed; the render thread's own per-frame steps (a fade counter, a scroll) read the same table. Two more cuts make the waits cheap. The frame limiter (main+0x5be430) yielded and polled the clock, which on the host spun 2 ms a frame on a performance core and ran at 59.5 updates a second; it now sleeps the time left. The render thread waits for the next update with a timeout of one frame given in milliseconds to a wait that takes microseconds, so it polled every 16 µs (about 50,000 wakeups a second at 60 fps); it now waits a frame. Prologue battle on the Mac: 59.9 fps, 55% of a core and 1.8 W of CPU energy (the original at 30 fps: 44% and 1.7 W). The render thread also forced interval 2 while either of two flags was set: one for the opening movie, one for scenes such as the character creation screen after it (the engine's own 60 fps modes dropped to 30 there to keep 1080p). A fourth cut ignores both (main+0x50fb7c), so those scenes run at 60 too; the movie still lasts 210.6 s (6,318 frames at 30 fps), as in the original. - Mac input: a fresh save needs
DM_KBD=<name>for the name entry, and its confirm dialogs default to No. From a new game into the prologue battle (frame-based at 60 fps, about 7 minutes):DM_KEYS="f720:A*185/90,f2400:Plus*35/438,f17400:Left~0.3*120/500,f17480:A*120/500,f17560:Left~0.3*120/500,f17640:A*120/500,f18020:Plus*100/562"; the game offers a save right before the battle. The repeating Left, A, Left, A reaches Yes whether a confirm dialog is open or not (A, Left, A loops once it starts on an open dialog: A answers No, Left changes the selection, A reopens it). A presses during the opening movie pause it, so the scenes after it start a few seconds later from run to run.
Super Mario Galaxy (010099C022B96000)
- Engine: the Wii game (codename Comet) on NERD's Hagi layer, SDK 18.3.5, update 1.3.1 (v327680). The PowerPC code is statically recompiled into
main(hagi.config:CpuJit false,Statifier true), so there is no JIT; Hagi stands in for the Wii hardware: GX on Vulkan, the DSP with its ROMs indata/dsp, the disc from the RomFS, HD texture packs indata/texture_replaceanddata/ondemand_texture_replace. Modules main + subsdk0 (movie) + sdk; subsdk1 (GLSLC), subsdk2 (nn::pcm) and subsdk3 / subsdk4 (NVIDIA's OpenGL and Vulkan drivers) are not loaded. - Memory:
main's bss is 2.9 GB (the emulated memory); the game never callsSetHeapSize, so the guest block is 1 GB. Footprint on the Mac 1.9 GB, 1.1 GB of it Metal. - Vulkan: 99
vk*imports through Hades' layer (hle_vk.m), which Galaxy extended: push constants, descriptor arrays, dual-source blending from an output at location 1, copies inside a render pass, linear images over their memory, timestamp and occlusion queries, pipeline caches,vkCmdDraw, image copies and read-backs, the GX vertex formats (USCALED / SSCALED, three components), viewport and scissor kept across render passes, the swapchain's INHERIT transform and the vi layer crop (04). - Shaders: Hagi generates its GX shaders as SPIR-V at run time. While it shows "Initializing system..." it creates 3,089 modules (the 56 built into
mainamong them) and 4,535 pipelines fromdata/database/pip/pipelineDB.pip, the same on every boot (about 15 s on the Mac). The RomFS has no SPIR-V file, so the NSP script'sVulkan shader capturestage, with the profile's key listvk_capture.tsv, boots the game in the Mac host withDM_VK_CAPTURE=<version dir>/vk_captureuntil every listed key is there (under a minute), thenvk_shaders.pytranslates the RomFS and the capture. Its two geometry shaders (GX points and lines, expanded to quads) are skipped: those draws come out as one-pixel points and lines. - Pointer: docked, Galaxy's pointer is the Joy-Con gyro, which the host does not have; handheld, it is the touch screen. The profile's
op_mode handheldstarts it in handheld mode (720p). On the iPhone, touches no gamepad control takes go to the game's touch screen (in the 3D layout, outside the right third of the screen, which is the right stick; all of them while a controller is connected); on the Mac the mouse is a finger, andDM_KEYStaps withT<x>x<y>(normalized picture coordinates). - CPU: Hagi polls with
SleepThread(100 ns), which the host used to return from at once: one thread kept a core at 100%. Sleeps under 20 µs now count as yields and get the yield back-off (03). - Not yet: GX points and lines (the geometry shaders), sampler LOD bias (GX uses it; Metal samplers have none, red line). The base NSP in hand was damaged (147 MB zeroed near its end: the two WebM prologue movies, which the storybook prologue does not use); it was packed with
--allow-damaged. - Mac input: from a fresh save,
DM_KEYS="f1500:A,f1950:T0.373x0.70,f2300:A,f2700:A,f3200:A,f3600:LRight,f3800:A,f4200:T0.247x0.426,f4500:A,f4800:LRight,f5000:A,f5400:T0.659x0.366,f5800:A,f6200:LRight,f6400:A,f7000:A,f8000:A,f9000:A,f10000:A,f11000:A"creates file 1 (Mario icon, Regular Mode) and plays the storybook prologue into the Star Festival (about frame 12,000). Screenshots of the handheld picture needDM_NVN_SHOT_OUT=1280x720(the raw swapchain image is 1920x1080 with the picture in its top-left 1280x720).
Super Mario Galaxy 2 (0100FD8022DAA000)
- Status: brought up on the Mac (title, file select by touch, the opening and the first storybook stage at 59 fps) and playable on an iPhone.
- Engine: the same Hagi layer as Super Mario Galaxy, update 1.4.0 (v393216), with Galaxy's
sdkand movie module builds (SDK 18.3.5);main's bss is 2.6 GB. Everything Galaxy needed from the Vulkan layer and the host carries over; the profile is Galaxy's with its own memory layout and shader list. - Memory: the movie player initializes its own StandardAllocator with virtual address memory, 384 MB (
main+0x62c280), which aborted the game at boot: the guessed 256 MB alias region holds nothing after the SDK's 1 GB address-region node area. A rewriter cut for thissdkbuild (shared with Galaxy) shrinks the node area to 62 MB, and the alias region is 512 MB; the guest block is 1.25 GB, about 4 GB of address space withmain. Footprint on the Mac 1.93 GB, flat over five minutes (1.0 GB of it Metal). - Shaders: 3,797 SPIR-V modules, all created about 7 s after launch (Galaxy: 3,089), captured by the NSP script as for Galaxy (
vk_capture.tsv); the same two geometry shaders (GX points and lines) are skipped. - Damaged NSP: the base NSP in hand fails the hash check on
data/texture_replace/MarioGalaxy.hixandMarioGalaxy_Sub.har; it was packed with--allow-damaged. - Mac input: from a fresh save,
DM_KEYS="f1600:A,f2000:T0.268x0.579,f2400:T0.641x0.778,f2800:T0.128x0.426,f3200:T0.641x0.778,f3600:T0.659x0.366,f4000:T0.8125x0.889,f4500:A*6/300,f6000:LRight+A~1.2*200/100,f6050:Y*200/100"creates file 1 (Mario icon, Regular Mode), plays the opening and walks the first storybook stage to the spin tutorial and on (about frame 11,000). DM_KEYS runs its steps one after another, so a hold blocks the steps after it: walking and jumping go in one step (LRight+A).
The Legend of Zelda: Tears of the Kingdom (0100F2C0115B6000)
- 60 fps at normal speed: the game picks a present interval each frame (2 = 30 fps, 3 under load) and steps its game time by 60 / fps. Stubs in its
patches.py(FRAME_STEP; the sites come from masagrator's FPSLocker config for this build) replace that logic with the host's frame step in whole vsyncs, as in Breath of the Wild: int fps = 60 / step. Prerendered movies hold 30. On the Mac (M4 Pro) the opening cave holds 60 at 1.000 of real time; with 12 ms added to every frame it drops to 30 after three late frames and stays at 1.000.DM_FPS_TARGET=30saves power,0gives the game's own step back (every stub then runs the original instructions). - Before the host has decided a step (the first frames): the stub steps by the measured frame time, capped at the target. Game time that ran ahead after a short frame is paid back by the next frames (the idea from OJZen/totk-mobile); frames longer than 1/15 s are not caught up.
- Render scale:
DM_RENDER_SCALE=0.5–1.0multiplies the size of the 3D views (1600x896 docked) and fixes it there; the game's own dynamic resolution runs only at 1.0. The HUD keeps its resolution. - Address space (GitHub issue #70): the game's resource heaps are address regions in the alias region (2.375 GB in the 5 GB guest layout, 64 GB on the Switch). With the SDK's ~1 GB node area at its start, the alias region was used up to +2.25 GB at the first descent from the Great Sky Island with 1.00 GB mapped, and the game then took a memory shortage: its frame check (main+0x1fd32ec) logs
MSF, thenRandRf(reason bits of one resource heap's object), and plays the memory-shortage Blood Moon, which puts Link back at the start. A rewriter cut on thesdkbuild (the one Breath of the Wild 1.9.0 also uses) shrinks the node area to 62 MB, as Fire Emblem Engage's, Hollow Knight: Silksong's and other games' builds have it: the high-water mark is +1.75 GB there, and no shortage came in 3.7 minutes on the island (it came within a minute in 7 of 8 runs without it). Route from the issue's save on the Mac:DM_KEYS="20:A*6/4,f2480:LUp~5"(title, load, jump, steer into the pond).
Monster Hunter Rise (0100B04011742000)
- Engine: Capcom's RE Engine, SDK 11.4.3, update 16.0.2 (v2228224, Sunbreak). The base game is an XCI (cartridge dump; nx_extract reads its secure partition), the update an NSP, and its 252 DLC NSPs are licences only.
mainhas 125 MB of text: the rewriter puts its patch area right before the base (BIG_TEXT, a second pass withDM_PATCH_FRONT). - Files and shaders: everything sits in KPKA PAK archives, later patch PAKs overriding earlier ones by path hash. The 63,111 shaders are in SDF files (
shader_split.pykpka_jobs), with combined and separate texture tables at once (separate binds are texture slots 128 + n). - GPU heap: one 64 GB sparse virtual pool built from 4 MB physical pools. The host aliases mapped pages (
vm_remap) and keeps a 4 KB page table the replay reads textures through (streamed textures 32 GB in, mip tails at 4 KB offsets). The scene is GPU-driven (MultiDrawElementsIndirectCount). - Keyboard: the hunter, Palamute and Palico are named through the inline keyboard (
DM_KBDon the Mac, the input box on the iPhone); the name is masked throughngc::ProfanityFilter's vtable, which the host fills. - Frame rate: 60 fps (30 on the Switch). The frame loop (main+0x6b106c8) sleeps until
[obj+0x430] / [obj+0x434]sixtieths of a second have passed and steps the game by the time that really passed; the game sets the limit at[main+0x1295b3f0]+0x434to 30 after boot and asks for present interval 2. The host writes the profile'sfps_targetthere (hle_fpslock.c, FPSLocker's address for this build) and the profile'spresent_interval displayignores the interval; both are needed.DM_FPS_TARGET=30gives back 30, and the MacDM_KEYSroutes below count frames at 30, so run them with it. - Mac input: a fresh save (with the first-boot notices accepted) reaches character creation with
DM_KEYS="f4100:A~0.3,f6300:A~0.3,f7000:A~0.3,f7600:A~0.3". WithDM_KBD=Hunter,DM_KEYS="f4100:A~0.3,f6300:A~0.3,f7000:A~0.3,f7600:A~0.3,f8000:A~0.3,f8150:Down~0.25*6/40,f8450:A~0.3,f8650:A~0.3,f8900:A~0.3,f9100:A~0.3,f9500:A~0.3,f9800:Down~0.25*9/40,f10250:A~0.3,f10450:A~0.3,f10700:A~0.3,f10950:A~0.3,f11300:A~0.3,f11800:Down~0.25*10/40,f12250:A~0.3,f12450:A~0.3,f12700:A~0.3,f12950:A~0.3,f13300:A~0.3,f16000:A~0.3,f16300:A~0.3"goes on through creation (hunter, Palamute, Palico, each named) and saves to slot 1. Holds under 0.25 s are not seen, and 0.5 s repeats in lists. From a save made that way,f4100:A~0.3,f6300:A~0.3,f6800:A~0.3,f7100:A~0.3,f7500:A~0.3continues into the opening.
Ace Attorney Investigations Collection (010005501E68C000)
- Status: brought up on the Mac (first-boot settings, title, Investigations 1's menus, Episode 1's opening and first investigation up to the first Logic prompt; 25 minutes with no red line) and playable on an iPhone. Much of the bring-up was worked out first by armyja in a fork (GitHub issues #17, #18, #19 and pull requests #15, #16).
- Engine: Unity 2022.3.17f1 IL2CPP, SDK 17.5.3, update 1.0.0.2 (v131072). The base game is an XCI; its DLC NSP is a licence only. Two IL2CPP thunks end in an indirect tail call through
x18(main+0x1d271bc,+0x204ebec), which the rewriter now recognises (03). - Shaders: 4,951 (2,475 vertex / pixel pairs and one geometry shader, which Metal cannot express) in the player's serialized files and the Addressables bundles. Unity 2022 keeps each program's parameters as blob entries of their own and dropped the subprograms' local keywords; the player's files borrow the
Shadertype tree of theunitybuiltinshadersbundle (04). Binding layout as Engage's. - Files: the C# file API goes through the SDK's C library (
File.Exists→stat→open), which checks every path withnn::fs::IsPathMountedand the length-limit queries before callingnn::fs. The host answers them now; before that Addressables found nosettings.jsonand the game crashed on a null asset (main+0x31100ec). - Clock: the C library's real-time clock reads
nn::time::GetStandardUserSystemClockCurrentTimespec, now the host's clock;std::chrono::system_clock::now()calledstd::terminateright after the first dialog. - Memory: the guest block is Engage's 5 GB with the same node-area cut on this
sdkbuild, and a 1 GB application pool: StandardAllocator commits only 0.19 GB, and with Engage's 2.5 GB pool its reservation left Unity's own pools so little address space that the live regions reached the end of the alias region after 140 s (ten minutes: +3.91 GB of 4.375 with a 1.5 GB pool, +2.85 GB with 1 GB). On the Mac about 1.55 GB resident and 1.25 GB of Metal allocations, flat from the first investigation on (25 minutes; address space at +2.81 GB). - Frame rate: 60 fps, as on the Switch (present interval 1).
- Mac input: a fresh save goes through the first-boot notices (the privacy prompt's second button is "Next"), the title and Investigations 1's Episode 1 with
DM_KEYS="f600:A~0.3,f1200:A~0.3,f1800:A~0.3,f2400:A~0.3,f3000:Down~0.3,f3200:A~0.3,f3800:A~0.3,f4400:A~0.3,f5000:A~0.3,f5600:A~0.3,f6200:A~0.3,f6800:A~0.3*270/120"(the first investigation in Edgeworth's office at about frame 18,000; at about frame 30,000 the first Logic prompt waits for L).
Unicorn Overlord (010069401ADB8000)
- Status: brought up on the Mac (logos, title, a new game and the opening story scene; no red line) and playable on an iPhone.
- Engine: Vanillaware's engine, as in 13 Sentinels, SDK 16.2.4, update 1.05 (v262144). Its ExeFS holds only main and sdk. All data is in 7 CRI CPK archives; audio is CRI ADX2 with Opus streams. Both DLC NSPs are licences only.
- Shaders: 2,188 GLSLC outputs compiled into main (1,725 distinct: 614 vertex, 1,111 pixel), single-stage programs combined per draw. Every binding decodes.
- Service calls: three host changes, each found by one boot (03):
- the hardware multi-stream Opus decoder's C interface, forwarded to the SDK's software decoder;
nn::crypto::GenerateCryptographicallyRandomBytes, which the sdk's owngetentropycalls understd::random_device;- the profile key
alloc_big_alignit shares with 13 Sentinels. Without it, a ring heap cut from a memory pool kept base 0 and the game wrote to0x268right after the title screen.
- Memory: no system resource; the game sets a 3.12 GB heap (pool − used), which the 4 GB layout's heap region holds. The rewritten modules are 92 MB (
mem_used). On the Mac about 2.9 GB resident in the opening scene. On an iPhone (A20 Pro): a 2.88 GB footprint with at least 3.2 GB of headroom. - Frame rate: 60 fps. The game sets present interval 1; on the iPhone the frame time is 16.7 ms at the median.
- Mac input:
DM_KEYS=f2400:A*20/300goes from the title screen (about frame 2,500) through a new game into the opening story scene (about frame 4,900).
Octopath Traveler 0 (01005270232F2000)
- Engine: Unreal Engine 5 (the paged global distance field shaders and the LowLevelTasks task system) on its mobile renderer, SDK 19.3.13, update 1.0.5 (v327680); the base game is an XCI. Everything is in one pak,
pakchunk0-Switch.pak(AES-encrypted index; the key is found inmainand never printed), with CRIWARE sound banks and a USM movie beside it. - Shaders: 10,324, in the pak's two shader code archives, each one Oodle-compressed. The NSP script decodes them on the Mac with the game's own
OodleLZ_Decompress(as for any game with Unreal paks:dm/gfx/tools/uepak.py,DM_OODLE_JOB), about 10 s for all of them. 61 vertex shaders (particles) pick a texture slot at run time; the profile keytex_dyn_slotsbinds them (04). 2,084 shaders do atomics on buffer images, now bound to the image's memory. 16 compute shaders with 3D image atomics (Unreal's paged global distance field, which the mobile renderer does not dispatch) do not build. - Memory: Unreal reserves one address region (62 GB on the Switch) and its allocator, FMallocBinned3, 37 GB of it. A cut skips the allocator's constructor and the host runs Unreal's heap on a malloc zone (
hle_uemalloc.c); the region is capped at 2 GB (game_args) and the SDK's node area cut to 62 MB, so the guest block is 3.75 GB. 15 minutes into the story on the Mac: region mapped up to 1.26 GB of the alias region, Unreal heap 0.75 GB, 2.8 GB resident, 1.84 GB of GPU allocations. - Frame rate: 60 fps (30 on the Switch). The game's config caps Unreal at 30:
t.MaxFPS=30in[ConsoleVariables]ofDefaultEngine.ini(andFrameRateLimit=30inSwitchGameUserSettings.ini, which sets it again), andrhi.syncinterval=2inSwitchDeviceProfiles.ini, the present interval 2 the game asks for. The shipping build reads no console variables from the command line (no-ExecCmds,-DPCVarsor-ini:), so the host writes 60 and 1 into the two console variables (hle_fpslock.c, profile keyfps_target 60), whenever the game sets them back; the game then asks for present interval 1 itself. Unreal steps the game by the time that really passed: on the Mac,DM_NVN_REGLUMtraces of the same input script from a fresh save at 30 and 60 fps (frame time p50 33.4 → 16.7 ms) put the 1.5 s fade to white 70 s in at the same real time with the same shape, and both runs reach the same screens at the same times.DM_FPS_TARGET=30keeps the game's own 30. - Keyboard: the character's name and favourite dish come from the system keyboard (
DM_KBDon the Mac, the input box on the iPhone). The game keeps running behind it, so the guest clock does too (it used to freeze: the sound stuttered and the box lagged on the iPhone). - Mac input: a fresh save reaches the story with
DM_KBD=Lyra DM_KEYS="20:Plus,24:A*27/4,150:Plus,154:A*62/3,340:Right*12/3,341.5:A*12/3,380:Plus*5/3,396:A*20/3,470:A,474:Up,476:A,480:A*140/3"(initial settings, the prologue, character creation with three belongings, "Begin Lyra's journey?"). - Saves: the first save (about 13 minutes in) needs
fs::GetSaveDataSizeMax; four saves in a 26-minute run, and a later boot continues from them. - Status: brought up on the Mac through character creation and the story opening into free play (26 minutes from a fresh save with no red line, 2.74 GB resident), and playable on an iPhone: 30 fps through character creation, name entry and the story opening, no audio underruns, three saves, a footprint of about 2.35 GB after 3.4 minutes; it plays at 60 fps on an iPhone too (
fps_target 60). Battles are not tested yet.
Pokémon Legends: Z-A (0100F43008C44000)
- Status: brought up on the Mac: language select, character creation, the opening and the first minutes of play in Lumiose City at a steady 30 fps (a 5-minute walk: GPU about 6 ms a frame, no crash; the red lines are the render condition the host does not evaluate, the placeholder cube and an occasional memory-pool retry). Playable on an iPhone: 30 fps, with a memory footprint of about 2 GB.
- Engine: Game Freak's Trinity engine on SDK 19.3.11 (NVN 55) with Havok 2022.2, update 2.0.2 (v393216). The SDK 19 calls the host had to answer are in 03.
- Files and shaders: every file sits in the Trinity packs of
arc/data.trpfs(FlatBuffers, Oodle Mermaid). The shader split decodes them with the game's ownOodleLZ_Decompress, run in the Mac host's tool mode (hle_oodle.c; the NSP script sets it up for any game with Trinity packs), and finds 28,603 shaders (01). - Memory: the 5 GB guest block with two cuts for 2.0.2: the StandardAllocator's address space 12 GB → 4 GB and the SDK's node area 1 GB → 62 MB. With
mem_map_limitthe host refuses a map past the application pool, as Horizon does, so the SDK gives back the 3.15 GB it keeps mapped afternninitStartup. - Graphics: characters are skinned with transform feedback, the colour grading LUT is drawn as a layered pass, the deferred lighting reads stencil through a stencil-mode texture view, and the occlusion queries that decide how characters are animated need the draw's base instance in the driver segment (04).
- Fixed: on that route, the Mac host crashed in the driver's compute encoder end at about frame 3390 (the transform feedback draw opened its encoder beside the open copy encoder), and from about frame 5300 a fragment shader's image atomics, sent to the 64 KB dummy buffer, ran about 1.4 MB past it: the game's heap was corrupted (the game or the replay crashed) or the GPU page-faulted, after which macOS ignores the command buffers and the picture freezes. An iPhone 17 freezing with
kIOGPUCommandBufferCallbackErrorPageFaultwas reported in GitHub issue #101. The atomics now go to a scratch buffer as large as the image (04); three runs of the whole route reached frame 12,300 with no GPU error. - Mac input: a fresh save reaches play in Lumiose City with
DM_KBD=Taylor DM_KEYS="f450:A,f1200:LRight~1,f1600:A,f1900:Down,f2000:Down,f2100:A,f2400:A*80/90"; a save made there continues withDM_KEYS="f300:A*3/150"(play from about frame 600), andLUp~12then walks along the platform.
Cuphead (0100A5C00D162000)
- Status: brought up on the Mac with no host change: title, save slots, the opening storybook, Elder Kettle's house, Inkwell Isle One and Forest Follies at 60 fps with no red line; playable on an iPhone.
- Fixed: in about half the runs, sprites broke into dark slivers or went missing (a log in Forest Follies splintered for a whole attempt), and on the iPhone the picture flickered every fourth frame. The host reported a ZCull storage size of 0; Unity's NVN pool allocator, asked for a zero-size block per depth target, gave out the offset of a live block, lost that block from its map of allocations, and later freed it with a garbage size. Buffers allocated afterwards overlapped live ones, so copies read staging bytes another copy had overwritten (every fourth frame is the upload ring's period). The size is now never 0 (
hle_nvn.czcull_size). - Engine: Unity 2017.4.9f1 IL2CPP, SDK 5.5.0 (the oldest so far, NVN API 53.313), update 1.3.7 (v655360); its DLC (The Delicious Last Course) is mounted as add-on content 1. Unity 2017.4's serialized and
Shaderformats (unityfs.py,shader_split.py) and the rewriter's jump-table form (patch_nso.py) were found by armyja on this game (GitHub pull requests #1, #2). - Shaders: 3,118 (1,558 pixel, 1,560 vertex) from the player's serialized files and AssetBundles, plus 4 GLSLC outputs in
main. Every binding decodes. - Memory: no system resource, so no virtual address memory: Unity sets a 3.12 GB heap (application pool − used memory) in a 4 GB block. The rewritten modules take 80 MB, so the profile reports 84 MB of used memory (
mem_used); with the default 64 MB,SetHeapSizefails and the game aborts at its first allocation. On the Mac about 3.2 GB resident and 2.7 GB of Metal allocations, flat over the house and the map. - Saves: the game writes a slot when it reaches the map, not when the slot is picked.
- Mac input: a fresh save goes through the title (L+R), a new slot, the storybook and the house to the map with
DM_KEYS="20:L+R,26:A,32:A,38:A,44:A,50:A,56:A*20/3,120:LRight~4,125:A,127:LRight~3,131:A*10/2,152:LLeft~3,155.5:A"(the map at about 160 s). From that save,DM_KEYS="20:L+R,26:A,32:A,35:A,40:LRight~1.5,42:LDown~1.5,45:LRight~3,49:LUp~1,51:LRight~3,54.5:LLeft~0.5,55.2:LUp~0.25,55.6:LRight~2,58:LUp~0.2,58.4:LRight~0.25,58.8:LUp~0.2,59.2:LRight~0.25,59.6:LUp~0.2,60:LRight~0.25,60.4:LUp~0.2,61:A,63:A"crosses the bridge (its entry is narrow between the posts) and enters Forest Follies (play from about 68 s; Y shoots).
FINAL FANTASY TACTICS - The Ivalice Chronicles (010038B015560000)
- Status: brought up on the Mac: initial settings, the opening movie, the prologue and the Orbonne Monastery battle up to the player's first turn, at 60 fps with no red line (about 1.4 GB resident); playable on an iPhone.
- Engine: Square Enix's engine on SDK 19.3.9 (its
sdkis byte-identical to Hades II's), NVN, update 1.4.0 (v393216). The OpenGL driver bundled assubsdk0is not loaded. CRI Mana movies and ADX audio run in the game's own code. - Shaders: 306: 257 GLSLC outputs in the technique files (
resources_nx/release/master/shader/*.tec), whichshader_split.pynow reads likemain's 49, and one geometry shader Metal cannot build. - Job system: its pools wake one worker through a shared auto-clear event and relied on the workers sleeping in index order; a build cut makes a worker woken in another's place pass the wake on (its
patches.py; the boot hung in 4 of 6 runs) (03). - Graphics: UI textures are written by the CPU into uncached pools after creation (
tex_flush_upload,tex_dirty_upload), and the UI is composited into the window texture by fragment-shader image stores read back asr32ui, between game barriers inside one pass (04). - Mac input: a fresh save reaches the first battle turn at about frame 21,000 with
DM_KEYS="22:A*60/2,150:Plus,152:X~2.5,165:Plus,167:X~2.5,175:A*70/2"(A through the settings and the birthday, then + and a held X skip each part of the opening).
Absolum (0100A83022A22000)
- Status: brought up on the Mac: the logo movie, the licence agreement, the main menu, the campfire character select, the intro and the first level (the "Strike / Skill" tutorial and the cave after it), at 60 fps with real sound and saves; about 2.5 GB resident. Playable on an iPhone.
- Engine: FNA (XNA 4 in C#) compiled with .NET NativeAOT (its own GC, regions enabled), SDL3, FNA3D's NVN driver, Wwise, SDK 18.3.2, update 1.1.1 (v393216). NVIDIA's OpenGL and Vulkan drivers (
subsdk1,subsdk0) are not loaded;subsdk2is the movie module (H.264 logos). Epic Online Services comes as an NRO the host does not load; the game plays offline without it. - Code:
mainstarts its text with 1.7 MB of data, which the rewriter now leaves alone (dm/tools/nso_code.py). - Shaders: 162 GLSLC outputs in
shaders.nx.bin. The picture is flipped by a viewport swizzle, which the host now applies (04). - Memory: everything is virtual address memory: malloc's StandardAllocator (40 GB of address space, cut to 2.5 GB on this build; 1.75 GB of it is FNA3D's GPU heap), the .NET GC's region range (
game_env DOTNET_GCRegionRange=60000000: 1.5 GB instead of 3.75), and the SDK cuts for its node area and its 1 GB alignment of large regions. They take 4.13 GB of the 4.375 GB alias region in a 5 GB block (03). - Mac input: a fresh save reaches the first level at about frame 9,000 with
DM_KEYS="f1500:A,f1900:A,f2300:A,f2700:A,f3000:A,f3300:A,f3600:A,f4000:A,f4400:A,f4800:A,f5400:A*30/150"(any button, accept the licence, New game, the character select, then A through the intro and the first dialogue);LRightwalks, Y strikes and X uses the skill.
Captain Toad: Treasure Tracker (01009BF0072D4000)
-
Status: brought up on the Mac with no host change:
- the opening course, the cutscene that follows it and the title menu;
- the first save, the Episode 1 book and Plucky Pass Beginnings;
- the Special Episode's book (add-on content 1).
It runs at 60 fps with no red line. Three minutes in a course: about 950 MB resident and 690 MB of Metal allocations, flat. Playable on an iPhone.
-
Engine: Nintendo EPD Tokyo's engine (the
allibrary over sead / agl, as Super Mario 3D World and Super Mario Odyssey) on SDK 8.2.0, update 1.4.2 (v393216). Itssdkis the build Link's Awakening 1.1.0 runs.mainexports its symbols (48,804 defined), sosdis.py --findnames most functions. -
Shaders: 7,258, all with Mario's binding layout. The fin fur geometry shader does not build, as in 3D World.
mainimportsglslcCompilebut no module defines it, so nothing is compiled at run time (04). -
Graphics: agl's post-effect program samples its own attachment, as in 3D World, and here the 16x16 magenta blocks showed on the Mac too: the profile's
feedback_snap. -
Input: the profile plays it handheld (
op_mode): docked, the game's pointer (touch blocks, stunning enemies) is the Joy-Con gyro; handheld, it is the touch screen. The virtual gamepad starts in the 3D layout (right stick: camera). -
Add-on content: index 1 is the Special Episode; index 2 holds a 1-byte marker file.
-
Memory: no system resource; the game sets a 3.00 GB heap (pool − 64 MB) in the 4 GB layout.
-
Mac input: a fresh save starts in the opening course with control of Toad.
DM_KEYS="f900:LUp+LRight~2.0,f1030:LUp+LLeft~2.2,f1170:LDown+LLeft~2.8,f1345:LDown+LRight~0.4,f1375:LUp+LRight~2.0,f1500:LDown+LRight~1.5,f1595:LUp+LRight~1.4,f1685:LUp+LLeft~0.9"walks him to the star:- through the arch, up the back ramp, around the rock on the grass top and over the roof;
- the course clears at about frame 2,100 and the title menu comes at about 5,200, with the first save;
- A dismisses the title's notices.
From that save,
DM_KEYS="f600:A,f1000:LRight~0.3,f1300:A"starts Plucky Pass Beginnings (play from about frame 1,900). How the route was found (position probe, the stage map's guide rail) is in 05.
Brain Memory Training Academy (0100F5D01DA3A000)
- Status: brought up on the Mac: the title, the controls screen, the level select and the levels with their Level Complete / Level Failed screens, at 60 fps with real sound and saves, no red line; about 0.75 GB resident, flat over 20 minutes. Playable on an iPhone.
- Engine: Unity 2022.3.5f1 IL2CPP with FMOD on SDK 16.2.4 (its
sdkis Unicorn Overlord 1.05's build), base game 1.0.0;subsdk0is the movie module. - Shaders: 496 in the player's serialized files (
Data/*.assets,Data/Resources) andmain. There are no bundles, so no file carries aShadertype tree to borrow;unityfs.pyhas Unity 2022.3's layout written out (KNOWN_TREES, the hash Ace Attorney Investigations Collection's bundles carry). The built-in VR blit's geometry shader does not build (04). - Memory: virtual address memory as in Ace Attorney Investigations Collection:
nninitStartupgives StandardAllocator pool − used, which needs the node area cut on thissdkbuild (sdk_patches.py; Unicorn Overlord, which has no system resource, runs the same with it). Ace Attorney's 1 GB pool and 5 GB block; the live regions end at about +2.1 GB of the 4.375 GB alias region (03). - Input: controller only (stick or D-pad, A, B, +); the game never reads the touch screen, so it stays docked at 1920x1080 and the virtual gamepad starts in the 2D layout.
- Mac input:
DM_KEYS="f400:A,f700:A,f1000:A,f1200:Right,f1300:A"starts a new game, enters level 1 and clears it (Level Complete at about frame 1,350).
The Great Ace Attorney Chronicles (010036E00FB20000)
- Status: brought up on the Mac: the first-boot notices (the DLC found, auto-save), the title and its Options, a new game of The Great Ace Attorney: Adventures, the opening movie, the first episode's opening scenes and its trial, at 30 fps with sound and saves; about 1.1 GB resident, flat over 14 minutes. Playable on an iPhone.
- Engine: Capcom's MT Framework on SDK 10.4.1, NVN, base game 1.0.0 (v0). Files are ARC archives of zlib entries under
nativeNXx64/(01);subsdk0is the SDK's movie module, whose extractor runs natively while the host decodes the H.264 movies (VideoToolbox). - Shaders: 460 (230 vertex, 230 pixel) in one MT Framework shader package (
sa/NX_64/root.arc), whichshader_split.pynow reads (04); all build, every binding decodes. - Host: SDK 10 calls that reach a service, found from the import list (album notifications, store demo mode,
vi::ListDisplays,fs::RenameDirectory, save thumbnails, shop and friend applets, the profanity filter called by name), and two NVN calls (03). - Graphics: the textures are copied into a CPU-cached pool by the CPU after creation, with no flush of any kind (
tex_flush_upload,tex_dirty_upload; 04). - Add-on content: index 1 (
DLC.dlcp); the game reports it found and added to Special Contents. - Memory: no system resource; the game sets a 3.14 GB heap (pool − 64 MB) in the 4 GB layout.
- Mac input:
DM_KEYS="f700:A*1000/24"(A every 24 frames) goes through the notices, the title and a new game into the opening movie; the first episode's dialogue starts at about frame 6,000 and its trial at about 20,000.
STEINS;GATE RE:BOOT (01008BC024FDE000)
- Status: the logos, the title menu (its background is a movie), a new game through the prologue movies into the story, the config pages, save and quick load, at 60 fps with music, voices and movie sound; no red line in 15 minutes of story. Playable on an iPhone.
- Engine: M2's ADV engine (
advengine, m2lib, the E-mote player for the animated characters) on SDK 20.5.17, update 1.0.3 (v131072).subsdk0is the SDK's movie library: itsmovie::Extractordemuxes the 87 H.264 / AAC MP4 movies and the host decodes them (output 4, NV12 buffers). Everything else is in the game's own PSB archives (*_info.psb.m+*_body.bin), which it reads itself. - Shaders: 487 in five BNSH in
main(483 pixel, 4 vertex); RomFS holds none. - Host changes:
nn::audio::SetAudioOutVolume/GetAudioOutVolume(the movie sound's AudioOut, set every frame; 03); the storage-sharing texture table drops entries whose storage another texture takes over, which kept GPU memory near 1 GB where it used to grow with every picture shown (04). - Graphics: 1280x720 in a cropped 1920x1080 window in both modes, by the game's own choice; the backgrounds' scratches, dust and colour fringes look like the art's own film effect.
- Input: buttons only: it reads the touch screen in handheld mode but takes nothing from it, so the profile keeps the default docked mode and the 2D pad layout (ZL / ZR phone trigger; quick save on R3 needs the 3D layout).
- Memory: no system resource; the game sets a 3.05 GB heap in the 4 GB layout. About 1.0–1.1 GB resident in the story on the Mac.
- Mac input: A on the title, Right to CONTINUE, A, Right to QUICK LOAD, A, A on the save, Left to OK, A loads the quick save. The menu takes presses late while its background movie starts (100 to 500 frames after the title's A), so check a
DM_KEYSroute's frames with screenshots.
Big Brain Academy: Brain vs. Brain (0100620012D6E000)
- Status: brought up on the Mac: the title, Solo, enrolling a new student (birthday, profession, appearance, online features, the creepy-crawly filter), the main menu (Practice, Ghost Clash, Test), the Practice categories and activities from each (Whack Match, Species Spotlight, Frame Filler, Fast Focus, Flash Memory, Cubegame, the photo quiz, Tick-Tock Turn, Shadow Shift) with their results, coins and unlocks, at 60 fps with sound, the H.264 activity previews and saves, by controller and by touch. Playable on an iPhone.
- Engine: NintendoWare Bezel Engine 1.56 on SDK 11.4.3, NVN, update 1.2.0 (v131072). Nearly all of RomFS is in Bezel Engine archives (
.bea, zstd entries, 01);subsdk0is GLSLC (imported, never called:mainholds no GLSL source) andsubsdk1the movie module. - Shaders: 2,125 BNSH in the archives'
.bfshaentries (most of them the engine's own, inSystem.nx.bea), the layouts andmain;shader_split.pyreads the archives (bea_members, 04). The 12 geometry shaders of Bezel's line renderer do not build: the main menu's chart is drawn with it every frame (a line strip with adjacency expanded into thick lines, then copied out of the frame), so the chart's outlines are missing. Ten pixel shaders pick their texture at run time from one of two places in the game's uniform buffers; they do not load and their draws are skipped. None of the activities above uses them; one 15-minute run of activities met one of them (e10f24835bbaa5f6,03GamePerception.bnbshpk) and the process aborted moments later, before the host caught SIGABRT, so its cause is not known; a second 15-minute run was clean. - Host:
nninitStartupchooses virtual address memory or a heap byIsVirtualAddressMemoryEnabled, which the host answered yes for every game; the profile keysys_resourcereports this NPDM's none. SDK 11's Opus work-buffer call, the IR camera, cached Nintendo Account info, per-file data caches and SDK 11's socket overloads (03). The activity previews loop with Flush and Start from four threads, which corrupted the movie decoder (a host fix,hle_movie.c). - Input: the profile plays it handheld with Boost (
op_mode handheld,perf_mode boost): handheld, the game reads the touch screen (every menu and activity, and Party's two-player touch battles); docked it does not. Handheld Normal draws 1280x720, Boost 1920x1080. The buttons work as well; the virtual gamepad starts in the 2D layout. - Memory: no system resource; the game sets a 3.14 GB heap (pool − used) in the 4 GB layout. It creates 1.5 GB of NVN memory pools in its first 25 s (384 + 256 MB CPU-uncached, a 320 MB physical pool for sparse mappings, 192 + 64 MB of render targets), so about 1.3 GB is resident at the title and 1.5 GB in the first activity. Over 15 minutes of activities it rose to 1.86 GB in steps as each new activity loaded (pools 217 → 292, textures 444 → 1,173), flat in between.
- Mac input: from a fresh save,
DM_KEYS="f1900:A,f2400:A*9/150,f3800:Right*14/150,f3875:A*14/150,f6000:A*4/150,f6900:Down*5/60,f7250:A,f7500:A*17/150,f10050:A"enrolls a student (birthday 01/01/2000, the default appearance, no online features) and starts Whack Match from Practice at about frame 10,050 (<work>/run/enroll.sh). In handheld modeT<x>x<y>taps work too:f1900:T0.5x0.88taps Start on the title.
Patrick's Parabox (0100E44016C22000)
- Status: brought up on the Mac: the first-boot language menu (all ten languages drawn), the title, the hub, Intro 1 solved and Intro 2 with undo, reset and the menu, at 60 fps with music and saves (progress kept across a restart); no red line, about 0.42–0.48 GB resident, flat over four minutes. Playable on an iPhone.
- Engine: Unity 2020.3.30f1 IL2CPP with FMOD on SDK 13.3.7, update 1.0.1 (v65536);
subsdk0is the movie module, which the game never uses. - Shaders: 486, all Unity built-ins (
Data/Resources/unity default resources,unity_builtin_extra) plus the player's 11 GLSLC outputs inmain. There are no bundles, sounityfs.pyhas Unity 2020.3'sShaderlayout written out (KNOWN_TREES: the type tree of Engage's bundles, the same hash). The built-in VR blit's geometry shader does not build (04). The whole game draws with three shader pairs. - Fonts: its only font, Jost, falls back to the shared fonts for Japanese, Korean and Chinese text, which Unity's player opens with FreeType straight from
GetSharedFontAddress; the host gives a game that imports noScalableFontEngineHelper::Decodethe TTF itself (03). - Memory: the NPDM declares 16 MB of system resource (
sys_resource), so virtual address memory as in Ace Attorney Investigations Collection and Brain Memory Training Academy:nninitStartupgives StandardAllocator pool − used, which needs the node area cut on thissdkbuild (sdk_patches.py); their 1 GB pool and 5 GB block. The alias region is mapped up to +1.5 GB with 0.27 GB in use. - Input: controller only (stick or D-pad to move, A redo, B undo, X reset, + menu): it stays docked at 1920x1080 with the 2D pad layout.
- Mac input: a fresh save solves Intro 1 with
DM_KEYS="f700:A,f1000:A,f1300:A,f1600:A,f2400:Right,f2450:Down,f2500:Down,f2550:Down,f2600:Down,f3000:Up*3/30,f3090:Right*3/30,f3180:Down*2/30,f3240:Up*3/30,f3330:Left*4/30"(English, the title, into Intro 1 from the hub at about frame 2,700, solved and on to Intro 2 at about frame 3,500).
Celeste (01002B30028F6000)
- Status: brought up on the Mac with no host change: the logos, the title, a new file and the prologue (walking, jumping, the camera following Madeline), at 60 fps with music and sound, no red line; about 1.6–1.7 GB resident, flat over five minutes. Playable on an iPhone.
- Engine: MonoGame for the Switch: MonoGame's and the game's C# compiled to C++ by Sickhead's BRUTE into
main(78 MB with its bss), FMOD Studio (its banks inContent/FMOD/Desktop, sound through AudioOut), SDK 15.3.2, NVN, update 1.4.1.1 (v589824; the title shows v.1.4.1.0). Onlymainandsdk. - Shaders: 26 (20 pixel, 6 vertex) in MonoGame effects: the 13 XNB files in
Content/Effectsand SpriteBatch's built-in effect inmain, whichshader_split.pynow reads (mgfx_effect, 04). Every binding decodes. - Memory: no system resource (
sys_resource 0), so no virtual address memory: the game sets a 3.11 GB heap (pool − used memory) in the 4 GB layout. The rewritten modules need 100 MB of used memory (mem_used). - Input: controller only; the game maps its buttons by position, so B and X jump. The virtual gamepad starts in the 2D layout.
- Mac input: a fresh save reaches the prologue with
DM_KEYS="f1850:A,f2200:A,f2600:A,f3000:A"(the title, Climb, a new file, Begin; control of Madeline from about frame 5,000); thenf5100:LRight~1.0,f5400:LRight+X~0.3walks to the first ledge and jumps onto it.
DRAGON QUEST XI S: Echoes of an Elusive Age – Definitive Edition (01006C300E9F0000)
- Status: brought up on the Mac through the opening movie, the title (name entry, system settings), the prologue movie (skipped with +) and into free play at Cobblestone Tor (the hero, Gemma, the villagers, the HUD and the minimap), at 30 fps with sound and saves; no red line, about 2.1 GB resident (1.1 GB of it Metal allocations), 1.5 GB mapped in the guest. Playable on an iPhone.
- Engine: Unreal Engine 4.18 on SDK 7.3.2, update 1.0.4 (v262144), with Square Enix's own sound driver (
SQEX::Sd, "SEAD") on the audio renderer;subsdk0is the movie module, driven by Unreal's Switch movie player (VP9 with two 6-channel Vorbis tracks). - Files and shaders: legacy version 4 paks whose blocks are LZ5-compressed, decoded on the Mac with the game's own
LZ5_decompress_safe(uepak.py,DM_OODLE_SERVEafternninitStartup, since the decoder uses the game's malloc). The 38,892 shaders are inline in the cooked packages and inGlobalShaderCache-GLSL_SWITCH.bin, each a zlib stream of anLSLGrecord (shader_split.pyue_legacy_jobs). The colour-grading LUT's slice-writing geometry shader is folded into its vertex shader's layer entry points (prog_fold_gs); the other 22 geometry shaders do not build (04). - Sound and movies: SEAD refills its voices without waiting on the renderer event, so catch-up frames wait for queue space. The movie player registers its decoder events before filling them (read at signal time), plays only when every track holds a decoded output (audio decoders keep 32), and after a skip sends an end-of-stream packet for every input it gets (none is handed out after end-of-stream) (03). Its 6-channel Vorbis tracks decode right only since stb_vorbis's fix for type-2 residues with more than two channels (README); before that, the movie sound came out distorted, with peaks far past full scale.
- Memory: Unreal's one address region is capped at 4064 MB (
game_args, MB up to 4095); two cuts on thesdkbuild (node area 62 MB, 32 MB alignment for regions of 1 GB and more) put it 64 MB into Engage's 5 GB block. Its cube-map array (510 faces) needed NVN's layer-face count (04). - Frame rate: 60 fps (30 on the Switch). The profile's
fps_targetwrites 60 into this build'st.MaxFPSand 1 intonvn.SyncInterval(hle_fpslock.c, both threads' copies; the game's config sets 30 and 2);DM_FPS_TARGET=30(Documents/env.txton the iPhone) keeps the game's 30 and saves power. On the Mac (M4 Pro) the field holds 60 fps (frame time p95 17 ms, GPU 6.6 ms p50 / 9.7 ms p95) at normal speed, since Unreal steps the game by the time that really passed, with sound intact. The game's dynamic resolution budgets are for 30 fps (r.DynamicResolution.*TimeBudget28–35 ms inSwitchEngine.ini), so it does not lower the resolution to reach 60: where a frame needs more than 16.7 ms, frames are dropped instead. - Mac input: with the save of an earlier run (the name entered),
DM_KEYS="20:A*150/3,250:Plus*4/10,480:B,483:LUp~4"skips the prologue movie at about 270 s and walks on Cobblestone Tor at about 480 s.
Monster Train 2 (010051701FB46000)
- Status: brought up on the Mac: the title, the 90 s intro movie with its sound, the tutorial battle (deployment, three waves, enemies on two floors), the merchant and the route map, at 60 fps (30 on the Switch) with music and saves (the intro plays once per save); no red line, about 2.17 GB resident and 1.21 GB of Metal allocations, flat over twelve minutes. Playable on an iPhone.
- Engine: Unity 2022.3.54f1 IL2CPP with FMOD on SDK 18.3.5, update 1.1.1 (v131072); its
sdkis Super Mario Galaxy's build, andsubsdk0is the movie module, whosemovie::Extractordemuxes the intro (StreamingAssets/intro_fmv.mp4, H.264 High and AAC). - Host: three general fixes (03):
svcGetInfotype 24 (FreeThreadCount), which the host answered 0, so Unity never created the thread its main thread waits for at boot; 64 output slots for a movie's audio decoder (8 when the game was brought up, 32 since DRAGON QUEST XI S): with 8, Unity's movie player held every slot and the intro hung on a black screen, and it peaks at 30;nvnSyncSetDebugLabel(a no-op). - Shaders: 15,937: 15,841 vertex, pixel and geometry shaders in the player's serialized files and bundles, the player's 11 GLSLC outputs in
main(one of them compute), and 85 kernels of the post-processing stack'sComputeShaderobjects inData/sharedassets2.assets, which the split now reads (unity_compute, 04); without them the colour grading LUT was never baked and the 3D battle scene stayed black behind the UI. No file carries a type tree for them, sounityfs.pyhas Unity 2022.3.54f1'sComputeShaderlayout (KNOWN_TREES); theShaderobjects use the 2022.3 layout already there. The built-in VR blit's geometry shader does not build. - Memory: the NPDM declares 16 MB of system resource (
sys_resource), so virtual address memory with no heap, as in Ace Attorney Investigations Collection, Brain Memory Training Academy and Patrick's Parabox, on the node-area cut thissdkbuild already had (sdk_patches.py); their 1 GB pool and 5 GB block. With the hardware pool StandardAllocator's region alone reached +3.9 GB of the 4.38 GB alias region innninitStartup. The alias region is mapped up to +3.25 GB within 90 s and stays there (+3.5 GB in a run to the first shop), at most 1.75 GB of it in use. - Input: controller only: the game reads no touch, docked or handheld (taps on the title and on route map nodes in handheld mode did nothing). It stays docked at 1920x1080 with the 3D pad layout (
pad_layout full), whose right stick scrolls the route map as the game's prompt shows; A selects and plays cards, B goes back, Y ends the turn. - Frame rate: 60 fps (30 on the Switch). The game asks for present interval 2 (Unity's 30 fps setting) and nothing else holds it at 30: the profile's
present_interval displaypresents a frame every display tick, and the iPhone's display link runs at 60 Hz. The GPU takes about 2 ms a frame in battle and on the route map (M4 Pro). Its animations follow the time that passed (Unity'sdeltaTime; the PC versions run at any refresh rate): from one save, screenshots every third of a second through the move to the merchant (its fade-in, the greeting) match at 30 and 60 fps, mean difference 3.9 against 11.0 for what frame counting would show.DM_PRESENT_INTERVAL=gamekeeps 30. At 60 fps a read into a texture pool lost a race with the dirty-page tracker's re-protection (two runs in three; 03). - Mac input: from a fresh save,
DM_KEYS="14:A"starts the game; the intro movie plays to its end (A, B and + do not skip it) and the tutorial battle loads at about frame 3,400;DM_KEYS="14:A,110:A*90/2,111:Y*20/9"plays through it to the merchant by about frame 8,000. A save past the intro starts at the title, and A there continues the run.
Raging Loop (010020400E1C2000)
- Status: brought up on the Mac: the publisher logo, the opening movie with its sound, the title, options, a new game through the tutorial and the prologue (the date card, the opening pictures, choices, the in-game menu, scenario chart and log), at 60 fps as on the Switch (the game asks for present interval 1), with saves; no red line at run time, about 0.6 GB resident at the title and 0.77 GB after ten minutes of story (it grows with each new picture and slows down), 0.4 GB of Metal allocations. Playable on an iPhone.
- Engine: Unity 2018.3.10f1 IL2CPP with FMOD on SDK 7.3.2, base game 1.0.0 (v0); its
sdkandsubsdk0(the movie module, which playsStreamingAssets/Movies/*.mp4, H.264 Main and AAC) are DRAGON QUEST XI S's builds, so that build's cuts apply as they are. The story's pictures are loose PNG / JPG files inStreamingAssetsthat the game loads itself. No host change was needed. - Shaders: 452: the 48
Shaderobjects of the player's serialized files (Data/*.assets, the Unity built-ins inData/Resources; 450 subprograms) and the player's 4 GLSLC outputs inmain. There are no bundles, so no file carries aShadertype tree to borrow;unityfs.pyhas Unity 2018.3's layout written out (SHADER_2018_3inKNOWN_TREES, from AssetRipper's TypeTreeDumps, 04). The two built-inComputeShaderobjects stay unread (a red line in the split); the game dispatches no compute. - Memory: the NPDM declares 16 MB of system resource (
sys_resource), so virtual address memory with no heap, in Engage's 5 GB block with the hardware pool (3.2 GB). Unlike Unity 2020 and 2022, Unity 2018.3 reserves no address regions of its own: every page the game maps lies in StandardAllocator's region, which ends at +3.21 GB of the 4.38 GB alias region, so the pool can stay as on the Switch (377 MB mapped at the peak over the title, the tutorial and the opening,DM_SVCmap log). - Input: controller only: the game reads no touch, docked or handheld (a handheld run with taps on the title's READ logged no touch read). It stays docked at 1920x1080 with the 2D pad layout; menus answer the left stick as well as the D-pad, and no menu item is selected until a direction is pressed. A advances and selects, B goes back, X opens the menu, Y the scenario chart, L the log, R auto mode.
- Mac input: from a fresh save,
DM_KEYS="f700:A,f900:LLeft,f1000:LLeft,f1100:LLeft,f1200:A,f1500:A*14/120,f3300:LDown*5/60,f3640:A,f3900:A*35/120"skips the opening movie, starts READ, reads through the tutorial, picks "I'm good (to main story)" and reads into the prologue (the motorbike picture at about frame 6,600).
Paper Mario: The Thousand-Year Door (0100ECD018EBE000)
- Status: brought up on the Mac: the title, a new file, the storybook prologue, Peach in Rogueport, Mario's house, the letter and the boat into Rogueport, with Mario under control on the docks, with music and sound, at 60 fps (30 on the Switch) and at the game's normal speed; no crash in eight minutes. Playable on an iPhone since the spin waits (CPU, below); the first build ran Rogueport at 24 fps there.
- Engine: Intelligent Systems' engine on NintendoWare (G3d, Vfx, Atk) on SDK 15.3.2 (Celeste's
sdkbuild), NVN, update 1.0.1 (v65536). Nearly every RomFS file is zstd, named.zst; the event scripts and data tables are ELF files the engine reads as data.subsdk0is the SDK's movie library: WebM movies (VP9 + Vorbis) throughmovie::Extractor, decoded by the host. - Shaders: 11,858 BNSH in
shaders/*.bfsha.zstandeffect/particle/*.ptcl.zst, whichshader_split.pyfound only once it decompressed zstd by magic rather than by the.zsname. 2,386 pixel shaders pick their texture between bound slots at run time (profile keytex_dyn_slots), and the reflection shaders index an array of handles in a uniform buffer, which the host now binds (04). The 4 domain shaders oftessellation.bfshahave no Metal stage; nothing seen so far draws with them. - Host changes:
nn::settings::GetAvailableLanguageCodes; a rewriter rule forsub sp, sp, xN, which the game's script call bridges use with sizes that leave sp misaligned (03);nvnCommandBufferClearColori/ClearColorui, a format-converting copy into a texture made without render-target use, and an unbound uniform slot reading zeros (04). - Frame rate: 60 fps (30 on the Switch). The game asks for present interval 2, fixed at startup, and steps its clocks in 60 Hz vsyncs: every frame
main+0x2e7750measures the time since the last one (oe::GetProgramTotalActiveTime), rounds it to vsyncs and clamps it to at least 2, and the base clock (main+0x2e753c; the field and battle clocks below it, the scripts' frame counts, timers and fades follow it) advances by that step. Presented every vsync without a change, it ran at double speed (each fade took half its time). Two changes on 1.0.1'smain(itspatches.py): the floor 2 → 1, and a frame-step stub (FRAME_STEP) that gives the base clock the host's step, the vsyncs each frame stays on screen, so a frame that takes between one and two vsyncs does not round the game slow; the log'sframe step: game time … of real timereads 0.994–1.003. With the profile'sfps_target 60the fades take as long as at 30 fps (the same press script,DM_NVN_REGLUMbrightness curves).fps_hold off: holding late frames made them slower (the frame waits for three render worker threads, idle during a hold), and the hold stuck at 20 fps on a loaded Mac.DM_FPS_TARGET=30keeps the game's 30. - CPU: the first iPhone build ran Rogueport at 24 fps: frame p50 42 ms against 8 ms of GPU time, with the three
gfx::ParallelManagerworkers (pinned to cores 0–2, priority 45) at 94% of a core each and the main thread waiting for them.main+0xa8e680is their loop. While a batch is pending, an idle worker polls the job cursor (main+0xa8e784) through a serial job, which only worker 1 runs, and through the end of a parallel-for, registering with it and backing out. The first worker to finish a parallel-for polls until the others are done (main+0xa8e9a8). None of these polls makes an SVC; they are what the general spin wait rule now handles (Spin loops, above). - Memory: 8 MB of system resource (virtual address memory) and no heap: the game's malloc reserves 62 GB of address space, cut to 4 GB on
main, with Absolum's node area and alignment cuts on thissdkbuild, in Engage's 5 GB block. It maps 1.5 GB at boot and 2.0 GB by Rogueport; about 2.4 GB resident on the Mac there. - Input: buttons and sticks, docked (1920x1080). The D-pad opens the Gear and Journal menus, so the virtual gamepad starts in the 3D layout (
pad_layout full). - Mac input: a fresh save reaches Rogueport's docks with
DM_KEYS="35:A,45:A,55:A,65:A*195/2"(the title, File 1, Start, then A every 2 s through the dialogue; Mario is under control at about 415 s, at 60 fps as at 30).
DARK SOULS: REMASTERED (01004AB00A260000)
- Status: brought up on the Mac: the logos and the opening movie, the title (offline), the first-run Brightness, Options and Button settings, character creation with the name through the inline keyboard, the opening cutscene and the Undead Asylum cell (walking, the camera, pillaging the corpse), at 60 fps with music and sound; no red line but the conditional rendering note; about 1.6 GB resident, flat over three minutes. Playable on an iPhone at 60 fps and the game's normal speed. Once, loading a save on the iPhone stayed on the loading screen (no save write, no red line); the next launch loaded it, and it has not come back.
- Engine: FromSoftware's dantelion2 engine with Havok and FMOD (sound through one 6-channel AudioOut) on SDK 4.5.0, the oldest so far, NVN API 53.313, update 1.0.3 (v196608).
subsdk0is the SDK's movie library: itsmovie::Extractordemuxes the H.264 MP4 movies (movww/,movjp/) and the host decodes them into the game's RGBA texture. Everything else is in the two dvdbnd archives (.bdt+.bhd5, DCX zlib entries, BND3 archives), which the game reads itself. - Shaders: 3,099 GLSLC outputs in six BND3 archives of
dvdbnd0.bdtand inmain, read byshader_split.py(bdt_shaders); SDK 4's GLSLC outputs are read through their section headers (glslc_section, which also gave Three Houses 8 and Age of Calamity 3 shaders that used to be dropped) (04). - Host changes: SDK 4's movie decoder constructor and its buffer without a sync object,
friends::GetBlockedUserListwithout aUid, the UTF-16 profanity masking by name, andPoll/Selectwith nothing to wait on sleeping out their timeout (the network thread kept a core at 100%) (03); handles read from uniform buffers take the base of the descriptor pool bound at replay, and the bound pools' bases live in each command buffer (the walls smeared sideways) (04). - Frame rate: 60 fps (30 on the Switch). The render thread ends each frame in the game's own CPU limiter, called with the constant 30 (
main+0x706fa4→main+0x707090); a cut makes it 60 (itspatches.py). The game does not step by the time that passed: its frame update takes a fixed 1/30 s (main+0x667904), so with the limiter alone it ran twice as fast on the iPhone; a second cut gives it 1/60, as the community's 60 fps cheat for this build does (03). Climbing ladders still misplays at 60 (with that cheat too). The stalls it had once a second at either rate came from the host's real-time declaration for that thread, which now covers the thread's peak frames (rt_comp_tickinhle_nvn.c). - Input: the game maps its buttons by position: B confirms, A goes back. The profile starts the virtual gamepad in the 3D layout (right stick for the camera, D-pad for spells and items).
- Memory: no system resource; the game sets a 3.10 GB heap (pool − used) in the 4 GB layout. The rewritten modules need 110 MB of used memory (
mem_used). - Mac input: from a fresh save,
DM_KBD=Ash DM_KEYS="20:A~0.2,25:Plus~0.2,40:A~0.2,50:A~0.2,60:A~0.2,70:B~0.2,80:Down~0.08,82:B~0.2,90:Up~0.08,92:B~0.2,100:Down~0.08,102:B~0.2,110:B~0.2,116:B~0.2,122:B~0.2*15/3,168:Left~0.1,170:B~0.2,185:Plus~0.2,190:B~0.2,200:Plus~0.2,210:B~0.2"goes through the two network notices, New Game, the first-run settings, character creation (a Warrior with the default look) and the opening into the Asylum cell (control at about 245 s). The steps are in seconds, not frames: the loads take different frame counts from run to run. From a save made there,DM_KEYS="20:A,25:Plus,40:A,50:A,60:A,70:B,76:B,82:B"continues into the cell at about 90 s.
Hollow Knight: Silksong (010013C00E930000)
- Status: brought up on the Mac: the logos, the title, a new file, the intro poem, the opening cinematic with its sound, and Hornet under control in the first two rooms of Moss Grotto (bind, jump, attack), with music and sound, at 60 fps as on the Switch (30 while a cinematic plays, as the game asks); memory flat in play. Playable on an iPhone. The base game alone (1.0.27776: another
main, the samesdkand movie module) packs from a derived profile with its own 2,244 shaders and plays the same opening on the Mac with no red line. - Engine: Unity 6000.0.50f1 IL2CPP with FMOD (one AudioOut) on SDK 20.5.8, update 1.0.30000 (v655360). Nearly everything is in 2,068 Addressables bundles (
StreamingAssets/aa/Switch);subsdk0is the SDK's movie library, whosemovie::Extractordemuxes the 27 MP4 cinematics (H.264 and AAC). While one plays the game draws 1280x720 with a window crop and present interval 2. - Shaders: 2,450: 1,189 vertex, 1,193 pixel, 67 compute and one geometry shader (which does not build), 11 of them the player's own GLSLC outputs in
main. The bundles carry their type trees (they report the engine version0.0.0), sounityfs.pyread Unity 6'sShaderobjects with no layout written out (04). - Host changes:
nn::hid::GetDebugPadStates, which Unity 6 polls every frame and the SDK's own aborts on (Result 0x2aca) (03). - Memory: 16 MB of system resource (virtual address memory) and no heap: Unity 6's
nninitStartupsets up no StandardAllocator, and Unity reserves and commits its memory itself (AllocateAddressRegion,AllocateMemoryPages). Engage's 5 GB block with the hardware pool, and the node-area cut on thissdkbuild (sdk_patches.py): the alias region reaches +2.26 GB in the first room (+3.00 GB without the cut), 1.5 GB mapped, about 2.15 GB resident on the Mac.profile_guess.pynow gives every game with a system resource the 5 GB block; it used to give heapless ones 256 MB of alias region. - Input: A binds, B jumps, Y attacks (the game's own prompts name A and B). It stays docked at 1920x1080 with the 2D pad layout; whether it reads touch in handheld mode has not been tried.
- Mac input: from a fresh save,
DM_KEYS="f1600:A,f1900:A,f2200:A,f2500:A,f2800:A,f9400:A~1.5,f9700:LLeft~60,f9800:B~0.5*60/50,f9830:Y*60/50"starts a new file in slot 1, plays the poem and the opening cinematic (control at about frame 9,200), binds, and walks left into the second room (its "B Jump" prompt), jumping and attacking as it goes.
Catherine: Full Body (0100BF00112C0000)
- Status: brought up on the Mac: the ATLUS logo, the title, the puzzle type choice, the Golden Playhouse intro, the anime opening (CRI movies with sound) and the first Nightmare stage with Vincent under control, no red line in a 6-minute run. Plays on the iPhone.
- Engine: Atlus's GFD engine on SDK 8.2.1, NVN, update 1.0.1a (v131072); CRI middleware: everything in five CPK archives (
data/*.cpk), ADX2 sound through one 6-channel AudioOut, Sofdec2 movies (data/movie*/*.usm) whose H.264 goes through the SDK'smovie::Decoder(the host's VideoToolbox decoder).subsdk0is GLSLC,subsdk1the SDK's movie library. - Shaders: 3,128 GLSLC outputs: 3,050 loose files in
data/shaders_binary, 76 inmainand 2 the game compiles at boot with its own GLSLC (captured on the Mac, as Three Houses'). This GLSLC (GPU code version 1.15) writes every vertex shader as a VertexA part (shader type 0, the position) and a VertexB part (the other outputs) in one code area;shader_split.pyreads type 0 as a vertex shader (sph_stage) anddmsh_build.pymerges the two parts as Ryujinx does (vertex_ab, 04). Without the merge every varying was lost and the screen was black. - Host changes: the GLSLC GPU code version range follows the game's own GLSLC (it refused to compile against SDK 7's 1.5–1.14),
nn::settings::GetAvailableLanguageCodeCount,nv12-uv-stridein the movie decoder's output format (CRI's player reads it unchecked), and a command-buffer commit right after buffer → texture copies, at the next pass boundary (the game reuses its staging memory about a frame after recording a copy; 03, 04). - Memory: no system resource; the game sets a 3.11 GB heap (pool − used) in the 4 GB layout. The rewritten modules need 96 MB of used memory (
mem_used). On the Mac (M6) in the first stage: 0.79 GB resident, 1.9 GB of Metal allocations. - Frame rate: 30 fps, as on the Switch (present interval 2). GPU p50 4.7 ms, CPU about a fifth of a core on the Mac.
- Input: + pauses a cutscene (B resumes, Y skips); the left stick moves Vincent, A grabs blocks.
- Mac input: from a fresh save,
DM_KEYS="5:A*25/2,60:Plus*14/4,61:Y*14/4,62:A*14/4"skips the intro scenes into the first Nightmare stage (about frame 3,300);120:LUp~0.4*40/1.5then climbs.
Digimon Story Cyber Sleuth: Complete Edition (010014E00DB56000)
- Status: brought up on the Mac: the logos, the title, the privacy policy, the DigiLab story choice, the EDEN chat opening (movies and real-time scenes), character creation and the login, into EDEN (the fly-in, then its entrance plaza in 3D with Taiga under control), with sound, at 60 fps in the menus and 30 from the story on (the game's own rates); no red line. About 0.95 GB resident (0.3 GB of Metal allocations in EDEN), flat over half an hour. Playable on an iPhone.
- Engine: Media.Vision's MVGL engine (the PC version's) on SDK 6.4.0, update 1.0.3 (v196608). It renders with OpenGL: NVIDIA's desktop GL 4.5 core profile through EGL, from
subsdk0, which is not loaded.maingets every GL function from a glad loader (eglGetProcAddress, 2,956 names) and calls 79 of them; the host implements those on Metal (dm/gfx/gl/hle_gl.m). Its NVN device serves only a helper beside the movie player. CRI's middleware plays the sound (one 6-channel AudioOut) and the USM movies (H.264 through the SDK's movie library,subsdk1, into NV12 buffers the game uploads into GL textures). - Files: everything but the movies is in nine MVGL archives (
DSDB*.nx64.mvgl:MDB1, a name tree, doboz-compressed entries;dm/gfx/tools/mvgl.pylists and reads them); four of them are CRI AFS2 sound banks under the same extension. - GL layer: one context; GL calls are encoded into Metal as they come, one command buffer per frame; GL's bottom-up orientation through the NVN layer's lower-left origin; textures made on first use from the levels given; buffers orphaned on
glBufferData; the window surface (the size of the game'svi::CreateLayer, 1920x1080) presented through the NVN layer's present path, paced by the swap interval (04). - Shaders: GLSL ES 3.20 source compiled at run time: 1,682
.shadfiles in the archives (615 distinct) and 37 strings inmain.dm/gfx/tools/gl_shaders.py <RomFS> <main> <dir> [--ios <dir>]translates them with glslang and SPIRV-Cross into<dir>/gl/(676 shaders: built-in strings are translated as each stage that compiles); the host finds each by the hash of its text.DM_GL_CAPTURE=<dir>writes every source the game compiles, whichgl_shaders.pyreads like a RomFS; every one captured on the route so far is in the archives ormain. - Host changes: the add-on content queries run on the big host stack (
aoc_loadlogs from a game thread with little stack left) (03);vi::CreateLayer(w, h)keeps the layer's size for the window surface. - Memory: no system resource; the game sets a 3.13 GB heap (pool − used) in the 4 GB layout. The rewritten modules need 74 MB of used memory (
mem_used). - Packing: for a GL game the NSP script runs
gl_shaders.py(Mac and iOS libraries in<id>_build/shader/glandshader/ios/gl) in place of the NVN shader split and chain build; it needs glslang (brew install glslang). - Mac input: a fresh save reaches EDEN with
DM_KBD=Taiga DM_KEYS="f2000:A*26/200,f7300:Right,f7400:A,f7600:A,f7800:Right,f7850:A,f8100:A*39/250,f9000:Plus*15/600,f18000:Down*3/1200,f18100:A*3/1200,f21700:Left*300/250,f21800:A*300/250"(the title, the privacy policy, the DigiLab: Cyber Sleuth, no options, A through the opening chat; the login form comes at frame 18,500 to 19,700). The name is a field of that form: A opens the keyboard, and the game's name screen stays up about 500 frames after the text arrives, where another A opens it again, so Down and A come 1,200 frames apart. The login prompt defaults to No, hence Left before each A from there; EDEN at about frame 22,500.
Animal Crossing: New Horizons (01006F8002326000)
- Status: brought up on the Mac: a new game through Tom Nook's counter (name entry on the game's own keyboard, birthday, look, hemisphere, island choice), the in-flight movie with its sound, the island's creation and the arrival (Timmy's welcome, the player at the dock under control), with the Happy Home Paradise add-on mounted; no red line but the ones below. Plays on the iPhone. A save is not reached on the Mac: the game first saves when the player sleeps at the end of the first day (the
−menu says "You can't save right now." before that); until then it only probes formain.dat,mainHeader.datandMoved/personal.datin its device save data. - Engine: Nintendo EPD's own (code name Park; sead / agl / gsys, NintendoWare G3d / Ui2d / Vfx2 / Atk / Font, Havok, Pia and NEX) on SDK 20.5.4, NVN, update 3.0.3 (v2228224);
subsdk0is the SDK's movie library. RomFS: 97,997 zstd files (.zs), BWAV sound, a few Yaz0 SARC archives; the in-flight movieMovie/OpeningMovie.webmis VP9 1080p with Vorbis, demuxed bymovie::Extractorand decoded on the host. - Shaders: 9,766 BNSH programs:
Shader/Park_UBER_All(3,385) and the other bfsha / sharcb inShader/, agl'sSystem/Agl/agl_resource(3,206), particle effects inEffect/(2,522), models' own bfsha inModel/, layouts inLayout/, the game's keyboard inSwkbd/and 40 inmain. The keyboard's ocean background has 8 geometry shaders, which do not translate; the name entry draws without them. - Rewriter: its 63 MB
mainwas the first to put branches out of range of the patch area: halfword jump tables now get only as many landing pads as their largest entry (half_count, 03); the patch area went from 42 MB to 7.9 MB. - Host changes: the game's own HIPC client for
olsc:u(cloud backup settings) is answered by host sessions (hle_sf.c); device save data is the host save directory, and a second mount name over one save shares its transaction; the adjustable user clock, the region code and SDK 20's album and steady-clock calls; video movie decoders keep 16 output slots (its player holds every one it is offered); and a run of yields restarts when the yielding loop's x19–x28 change, so loops that yield between steps of work run at full speed (the island's creation took 77 s of black screen; 03). - Graphics:
render_cond on: the game skips clears of targets nothing drew into with conditional rendering, as Tears of the Kingdom does; without it every frame was black. - Memory: no system resource; the game sets a 3.09 GB heap in the 4 GB layout. The rewritten modules need 122 MB of used memory (
mem_used). On the Mac at the dock: 1.85 GB resident, 1.15 GB of Metal allocations. - Frame rate: 30 fps, as on the Switch.
- Prewarm list: recorded from a fresh save through the counter, the movie and the arrival to the dock (965 lines).
- Input: the 3D gamepad layout (
pad_layout full): the D-pad holds the tool ring and the right stick turns the camera. - Mac input: from a fresh save,
DM_KEYS="40:A*51/2,145:B*12/0.4,151:Down,152:Right*4/0.4,154.5:A,155.5:Right*4/0.4,158:A,159:Down*2/0.4,160.5:Left*2/0.4,162:A,163.5:Plus,167:A*4/2,175:Plus,178:A*146/2,179:Plus*30/4"names the player "tom" on the keyboard, takes the defaults, watches the movie and reaches the dock at about frame 14,100 (470 s).
DRAGON QUEST VII Reimagined (0100A9D01C446000)
- Status: brought up on the Mac: the logos and opening movie with sound, the title, a new adventure log (name entry, settings), the opening cutscene (the well, the night scene with Maribel by the boat) into play at Pilchard Bay with the controls tutorial, and a saved log continued, at 30 fps with saves, and the opening cutscene into the first free play (the hero's house) again at 60 fps (
fps_target 60); no red line at run time apart from the geometry shaders below; about 2.6 GB resident (1.1 GB of Metal allocations) in the cutscene. Playable on an iPhone at 30 fps; 60 fps on an iPhone is not tested yet. - Engine: Unreal Engine 4.27 (HEXA DRIVE) on SDK 19.3.1, update 1.2.0 (v131072): IoStore containers beside one pak (
pakchunk0-Switch.pak, version 11, its footer magic 0xcc bytes from the end), the desktop renderer with forward shading, PhysX 3.4, CRIWARE sound (a 6-channel AudioOut) and Sofdec movies (VP9 through the SDK's movie decoder, mode 1, NV12).subsdk0is the movie module. - Shaders: 62,901 in the pak's two shader code archives, every one an LZ4 block (
uepak.pylz4_shaders; the NSP script runs no Oodle host for them). The game has no PC version, so the facts table types nothing; a size query on an image handle now takes the image's type (04). 62 of the 63 geometry shaders (one-pass cube shadow maps, six layers from one draw) do not build; their draws are skipped (point light shadows). - Host changes: the audio renderer at 32 kHz (its upsampler exists only for DSP rendering, so the host resamples 3:2 itself),
GetAudioDeviceOutputVolume, the shop and Nintendo Account calls of the title screen,kpr::GetKeyCodeMap, the movie decoder'snv12-uv-stride(grey movie in bands), stencil-mode texture builders (the hero dithered), and texture slots 32–39 reading the bound image (characters glowing white in cutscenes) (03, 04). - Memory: Unreal's MallocBinned3 reserves 37 GB of pools in the engine region, so its creation is cut and an observation gate hands Unreal the host's malloc zone (its
patches.py,hle_uemalloc.clayout UE4_27); the region is 2 GB (game_args -SwitchVirtualAddressSpaceSize=2048) with Octopath Traveler 0's node-area cut on this SDK build, in a 3.75 GB guest block. About 2.6 GB resident on the Mac in play (the Unreal heap about 1.25 GB, 0.9 GB of Metal allocations). - Frame rate: 60 fps (30 on the Switch). The game's config caps Unreal at 30 twice:
FrameRateLimit=30inDefaultGameUserSettings.iniandSwitchGameUserSettings.ini, whichUEngine::SetMaxFPS(main+0x134d2c0) puts int.MaxFPS, andrhi.syncinterval=2inSwitchDeviceProfiles.ini, the present interval 2 it asks for whiler.VSync=1(BaseSwitchEngine.ini) locks presents to vsync. As for Octopath Traveler 0, the host writes 60 and 1 into the two console variables (hle_fpslock.c, profile keyfps_target 60) whenever the game sets them back, and the game then asks for present interval 1 itself. StevensND's 60 fps mod for the base game'smain(buildab0602…; GameBanana) lifts the same two limits with patches in code: it returns at once fromUEngine::SetMaxFPS, sot.MaxFPSstays 0 (no limit), and it overwrites the first two characters of the UTF-16 namer.VSync, so the config'sr.VSync=1finds no variable and presents are no longer locked torhi.SyncInterval. Its "Uncap" variant also patches thesdkfor present interval 0. Neither patch touches how the game steps time, and Unreal steps the game by the time that really passed. On the Mac,DM_NVN_REGLUMtraces ofrun/to_maribel.shfrom the same save at 30 and 60 fps (frame time p50 33.4 → 16.7 ms) match at time scale 1.00 with a constant offset (−1.75 ± 0.2 s over the last 90 s, correlation 0.998–1.000), the night scene comes out at the same resolution, and no new red line appears.DM_FPS_TARGET=30keeps the game's own 30. - Mac input: the work folder's
run/ctl.shdrives a run started withDM_CTL(05);run/to_maribel.sh <run>goes from the title through a new adventure log into the opening cutscene's night scene by the boat (a save with an adventure log in the first slot).
FINAL FANTASY RESONANCE (0100606026F12000)
- Status: brought up on the Mac: the first-boot brightness screen, the title, a new game (Normal difficulty), the opening cutscene, the first field (Battlefield) with its autosave, Continue from that save, and the first two battles, at 60 fps with no red line apart from one moment of the new game's load (below). Playable on an iPhone.
- Engine: Unreal Engine 5.6.1 (Square Enix; the desktop renderer,
SF_GLSL_SWITCH), SDK 20.5.17 (thesdkandsubsdk0builds of STEINS;GATE RE:BOOT), update 1.0.1 (v65536); CRIWARE sound. Everything is in one IoStore container (FFRS-Switch.utoc/.ucas, 6 GB) besideFFRS-Switch.pak(configs and the like). - Rewriter: 94 MB of text, 95% of it without unwind entries (82 MB in one stretch, 6.65 MB before the first FDE). Two general changes: code before the first FDE is one function, and functions longer than 256 KB are analysed in pieces, without which the rewrite took 100 GB of memory (03).
- Shaders: 73,496 unique (9,253 vertex, 61,305 pixel, 2,934 compute, 4 geometry) in the container's two IoStore shader libraries, two layers of Oodle, read through the game's own decoder in the Mac host's tool mode with no boot (
uepak.py, 01). 11 do not build: 3 geometry shaders and 8 compute shaders with 3D image atomics, none met so far. 2,077 particle vertex shaders pick their texture slot at run time (tex_dyn_slots);CopyStencilToLightingChannelsPSwrites integers to anR8Uinttarget (shader_facts_manual.tsv: without it the lighting channels stayed empty and the sprites showed as dark silhouettes). - Memory: Unreal's FMallocBinned3 runs on a host malloc zone (a 32-slot FMalloc vtable in UE 5.6,
hle_uemalloc.clayoutUE5_6). The engine's page allocator only ever grows its use of the engine region (2.03 GB of address space after seven minutes, about 1 GB of it mapped), and a 2 GB region aborted in the first battle; the region is 4064 MB (game_args) in Engage's 5 GB guest block (03). About 2.3–2.6 GB resident and 1.2–1.6 GB of Metal allocations on the Mac in the field and the first battles, flat over seven minutes. - Frame rate: 60 fps (30 on the Switch): the host writes
rhi.SyncInterval1 (hle_fpslock.c); the game steps by real time (same luminance traces at 30 and 60 fps).DM_FPS_TARGET=30keeps 30. - Known: once in two runs, the frames that begin the new game's load bind 2D textures where a base pass shader declares 3D ones (red
texture type mismatchlines, under a black screen). - Mac input: the first boot shows the brightness screen; from a fresh save,
DM_KEYS="f600:A,f1500:A,f1700:A,f1850:Up,f1950:A,f2400:A*36/150,f7950:B,f8100:B,f8250:B,f8400:B"takes New Game, Normal, "Yes", the opening and the tutorials into the first field (about frame 9,600 at 30 fps; B closes a tutorial on its last page, the stick turns its pages). From a save made there,DM_KEYS="52:A,58:A,62:A"continues into the field;70:LUp~4walks north to the first battle. The work folder'srun/save_battlefieldholds such a save.
Luigi's Mansion 3 (0100DCA0064A6000)
- Status: brought up on the Mac: the boot notices, the title (the hotel in daylight), a new game and the opening cutscene (the bus ride, the arrival at the hotel, the lobby), then the lobby in play at 60 fps (30 on the Switch), about 3.7 GB phys_footprint (3.0 GB of Metal allocations). Playable on an iPhone at 60 fps.
- Frame rate: 60 fps (30 on the Switch). At the end of each frame the main thread calls its frame callbacks (
main+0xb7c308) with the milliseconds since the frame began; the frame limiter among them (main+0x7a3e70) runs jobs or sleeps until 33.333 ms have passed, a float in read-only data (main+0x13c94dc, read only there), and the game steps by the time that really passed. The host writes the profile'sfps_targetthere as a frame time (hle_fpslock.c: 16.167 ms at 60, half a millisecond under the vsync so the presents set the pace; at exactly 16.667 the work after the limiter made it 59.6 fps with a frame shown twice every 2.5 s), andpresent_interval displayignores the present interval 2 the game also asks for. On the Mac the lobby holds 60.0 fps (display interval p99 16.67 ms); the same 2 s walk and 1.5 s turn from the same save end at the same spot at 30 and 60 fps, and the title's animation matches at time scale 1.00 (DM_NVN_REGLUM).DM_FPS_TARGET=30keeps the game's 33.333 ms. The limiter also has a phase lock, a nudge of at most 0.05 ms a frame from the layer's frame history, which stays off: the host'snn::vi::ListFrameHistoryreturns no entries. - Engine: Next Level Games' own on NVN, SDK 7.3.2 (the
sdkbuild of Super Mario Odyssey), Havok physics and cloth, Wwise (a 6-channel AudioOut), NEX / Pia.subsdk0is a second copy of the SDK (nnSdkEn:sdk's code byte for byte, another name and build ID), which comes first in the load order, somain's imports bind to it;subsdk1is the movie module. No virtual address memory:mainsets a 2.94 GB heap. - Files: every RomFS file but the Wwise banks is an archive of the engine's own,
<name>.dictindexing zlib chunks in<name>.data; update 1.4.0 ships<name>.patchbeside the unchanged base files, rebuilt into each section the way the game does it (01).shader_split.py(nlg_shaders) reads both. - Shaders: 5,317 (5,149 in the base game), all in
init: control sections back to back in one section, their code areas in the same order in another, one NVN program per stage (paired as single-stage programs). 586 pixel shaders read their shadow cascades from an array of the driver table's handles (c2_array), and 1,396 write the G-buffer, an R32Uint and an RGBA8Uint target, as integers (fs_uint) (04). Not built: 10 geometry shaders (one expands a box from two corners) and the tessellation stages (15 hull, 18 domain shaders), whose patch draws are skipped. Neither is drawn much: from the title through a new game, the opening cutscene and the lobby, no draw used a geometry shader (the host now names one that does,draw with geometry shader … which has no library), and two tessellated programs drew twice a frame for about 30 frames, in the title scene behind the file select as it slides out before a new game. Metal 4, which the host draws with, has no tessellation stage: those draws would need a tessellator of the host's own. - Host changes: the game patches texture handles into its recorded command buffers in place, at the command start + 0x10 it took from
GetCommandMemoryUsed, so single texture binds are recorded with the handle there (OP_BIND_TEX1; without it the title showed a stretched icon and a new game a white screen); a depth texture drawn through a colour view of the same size (a shadow, copied back after the pass);nvnBlendStateSetAdvancedModeandnvnCommandBufferSetDebugLabel(03, 04). - Mac input: the work folder's
run/ctl.shdrives a run started withDM_CTL;DM_KEYS=14:A,18:A,26:A,31:A,41:Right,42:A,54:Agoes from the boot notices through a new game into the opening cutscene.
AI: THE SOMNIUM FILES (010089B00D09C000)
- Status: brought up on the Mac: the Spike Chunsoft logo movie, the title (its background movies), a new game (language, auto save notice), the opening at the merry-go-round and the first investigation with its conversation tutorial, both sticks; loading the autosave from the title. 60 fps (present interval 1), about 2.27 GB resident, flat over 5 minutes; audio through FMOD's AudioOut. Playable on an iPhone.
- Engine: Unity 2017.4.17f1 IL2CPP with FMOD on SDK 6.4.2, update 1.0.1 (v65536).
subsdk0is the movie module; the 168 movies (StreamingAssets/Movie, H.264 and AAC in MP4) play through Unity's video player, NV12 into CPU buffers copied to an R8 + RG8 texture pair each frame. - Shaders: 13,057 (6,572 vertex, 6,482 pixel, 3 geometry) in the AssetBundles of
StreamingAssets(Cuphead's Unity 2017.4 layout) andmain. All build for Mac and iOS. The 3 geometry shaders (Unity's DX11 depth of field, a wireframe effect) use the general native-VS-capture / GS-compute path; GPU readback tests check their output, but none was drawn in the opening observed so far. See geometry support and limits. The ComputeShader objects ofData/resources.assetshave no type tree (red line in the split, as in Cuphead); no dispatch has missed one so far. - Host changes: an H.264 drain held up by a full set of output slots (the logo movie never ended, 03), and the per-stage composite programs restored after the precompile prediction (04); both general.
- Memory: no system resource; Unity sets a 3.11 GB heap (pool − used) in the 4 GB layout,
mem_used104 MB for the rewritten modules. - Input: controller only, docked (1280x720 in the cropped 1920x1080 window, by the game's choice), with the 3D pad layout: the investigation moves its cursor with the left stick and the view with the right one.
- Mac input: the work folder's
run/ctl.shdrives a run started withDM_CTL. From a fresh save: 15 s, A (New Game), Down, Down, A (English), Left, A (Yes), then A every 3 s into the investigation (about 3 minutes). With the autosave:DM_KEYS="15:A,18:A,21:Left,22:A,30:A,40:A*45/3"loads it and plays the opening again.
ANIMAL WELL (010020D01AD24000)
- Version and status: update 1.0.10 (
v655360), SDK 20.5.7. The Mac host reaches the opening rooms at 60 fps with movement, jumping and audio; the unsigned IPA is confirmed playable on the iPhone. Only this update is registered in the profile. Item pickup and pause menus also work; save/reload remains unverified because the test did not reach a save checkpoint. One repeat run recovered from an AudioUnit startup timeout through the existing audio restart path. - Files: both the base and update Program NCAs have no data section. Assets live in
main, whose read-only segment is about 32 MB.nx_extractnow creates an empty RomFS directory (and a header-only survey manifest) when neither program has a data section. The profile's emptyromfs_markerchecks that directory. Base and update extraction were checked in both full and manifest modes. - Shaders: 46 BNSH shaders embedded in
main(1 vertex, 45 pixel), all translated for Mac and iOS by the existing splitter and translator. No shader overrides, rewriter cuts or host changes were needed. The prewarm list covers the opening rooms. - Memory and controls: no NPDM system resource; the provisional 3.5 GB guest layout holds, with a 256 MB alias region and a 256 MB stack region.
mainhas about 537 MB of BSS, counted by the existing big-module memory rule. During a four-minute Mac run, resident memory stayed near 855 MB and Metal allocations at 159 MB. The 2D virtual pad is selected; B jumps.
Mario Kart 8 Deluxe (0100152000022000)
- Status: brought up on the Mac from update 4.0.0 (
v1441792): boot, the title with its demo race, the Mii pick, the menus (their movie previews play), a 50cc Grand Prix race at Mario Kart Stadium to its results, and a second boot that loads the save. 60 fps (p50 16.7 ms), sound; about 2.4 GB resident and 1.26 GB of Metal allocations in the race, flat over minutes. Playable on an iPhone (A20 Pro): 60 fps docked with about 6 ms of GPU time a frame, a 2.4 GB footprint with 3.7 GB of headroom, no audio underruns. - Engine: Nintendo's sead / agl / gsys with NintendoWare (G3d, Ui2d, Vfx, Atk) and Pia, on SDK 4.4.0, the oldest SDK so far;
subsdk0is the movie module. No virtual address memory:mainsets a 3.03 GB heap in the 4 GB layout. - Shaders: 29,650 (12,529 vertex, 17,092 pixel, 28 compute, 1 geometry): BNSH in the .szs SARCs (courses, objects, karts, drivers), the effects, the layouts, agl's two sharcb and 37 in
main. The race records conditional rendering as a course loads; the profile hasrender_cond on. - Miis, without the firmware: the game builds Mii heads from the console's MiiModel system data, starting at boot for the title's demo race, and lists the six default Miis for its "Select a Mii". The host's Mii database has those six and no Mii of its own, and its MiiModel stand-in has every part empty, so Mii heads draw nothing while bodies and favourite colours show (
hle_mii.c, 03). - Host changes, all general: the Mii database and the MiiModel stand-in; the add-on content cache sized as Horizon's RomFS working memory (its licence is decrypted in a 0x3130-byte heap); device save data as a save of its own for a game that mounts account save data too (every boot started a new save); SDK 4's DeviceSink layout and the audio renderer's update lock read off each SDK's
Update(it was SDK 16's for every SDK before 20); and the zero cube for a one-layer 2D array in a cube slot (03, 04). - Mac input: the work folder's
run/ctl.shdrives a run started withDM_CTL;run/race.shboots from a copy of a save past the Mii pick into a 50cc Grand Prix (Mario, Mushroom Cup) with frame-timed keys and holds accelerate (DM_KEYS=f960:L+R~0.3,f1260:A,f1500:A,f1740:A,f2000:A,f2240:A,f2400:A,f2560:A,f2800:A,f3040:A,f4300:A~70). From a fresh save, L+R at the title opens "Select a Mii" (Right, A takes the first default Mii).
Balatro (0100CD801CE5E000)
- Status: brought up on the Mac from update 1.1.3 (
v851968): the intro, the title, a new run by touch and the tutorial's first steps, at 60 fps with sound; about 0.65 GB resident and 0.2 GB of Metal allocations, flat over three minutes in a run. Plays on the iPhone (IPA about 82 MB). - Engine: LÖVE 11.4 ("love-nx") with LuaJIT 2.1.0-beta3 in its interpreter and SDL, on SDK 18.3.2. The game is Lua source in the RomFS (
main.lua,game.lua,engine/,functions/), its pictures PNG, its sound Ogg Vorbis through OpenAL Soft on the audio renderer. It renders with OpenGL: NVIDIA's desktop GL 4.5 through EGL (subsdk0, not loaded;subsdk1is NVIDIA's Vulkan driver, andmainimportsvkGetInstanceProcAddronly for SDL's Vulkan loader, whichprofile_guess.pynow leaves out of its Vulkan count). LÖVE's glad loader asks for 2,133 GL names and its code calls about 150 of them (loader_census.py). - GL layer: LÖVE takes its core-profile paths when
GL_CONTEXT_PROFILE_MASKsays core (the layer now answers it): its vertex streams are persistently mapped buffers (glBufferStorage,glMapBufferRange) fenced per frame (glFenceSync,glClientWaitSync); with a compatibility profile it would draw from client memory. Also new for it: attribute bindings (glBindAttribLocation: LÖVE fixes VertexPosition, VertexTexCoord, VertexColor and ConstantColor to 0–3) and current attribute values (glVertexAttrib4f: VertexColor and ConstantColor are disabled arrays),glGetActiveUniformand the uniform read-backs, everyglUniform*form, texture storage with its levels, 3D and array textures (LÖVE makes a default of each), texture swizzles (its font glyphs are RG8 read as R, R, R, G),glGenerateMipmap(the game's images are mipmapped), instanced and base-vertex draws, separate read and draw framebuffers, blits andglReadPixels(04). - Shaders: LÖVE puts each shader's GLSL together at run time, in Lua, from its own header and built-in code and the game's 19
resources/shaders/*.fs: 35 sources (#version 330 core), all made byGame:start_upbefore the first frame. No game file holds them, so the NSP script captures them on the Mac (gl_capture.tsv) andgl_shaders.pytranslates them. A GLSLbooluniform is auintin SPIR-V;gl_shaders.pynow reports the declared type, since LÖVE'sShader:sendtakes only a boolean for a bool (the game stopped at its first card otherwise). - Sound: the game opens the audio renderer without an event, and the host started its render thread only for the form with one, so only the game's own updates rendered, and nothing played them: silent. Both forms now start it (
hle_audren.c). - Input and display:
op_mode handheld: the Switch build plays by touch there (SDL turns the touch screen into LÖVE touches and mouse presses), the iPhone's touches outside the gamepad controls, the Mac's mouse; docked it reads only the pads. Its window is the 1280x720 handheld display.pad_layout touch: the virtual gamepad starts in its new Tap layout (only the layout button; every touch goes to the game), since the 2D layout's corners cover Run Info, Options and the deck; the layout button brings back 2D and 3D. - When the game stops: on the Switch, Balatro's error handler quits at once, and love-nx's video worker is then deleted twice (its thread releases the worker that the closing video module also deletes), which crashes in
main+0x339c00. A Mac run that reaches this has hit a Lua error first: a copy of the RomFS with aprintat the top oflove.errhandinmain.luashows it (a capture run without the shader library stops at the firstShader:sendso, after every shader is captured). - Mac input: in handheld mode,
DM_KEYS="f1300:T0.35x0.86,f1700:T0.35x0.86,f2100:T0.5x0.68"taps Play at the title, Play in the run setup and the tutorial's Next.
ROMANCE OF THE THREE KINGDOMS 8 REMAKE (0100C2A01A730000)
- Status: brought up on the Mac from update 1.1.3 (
v851968); playable on an iPhone (IPA 16.8 GB). On the Mac: the logo movie and intro, the title and its attract demo, the main menu, a new game through the recommended setup (Ruler, Cao Cao, 195), its opening story on the map, the council (espionage, personnel, the help panel), a saved and loaded game, a month's missions, the city view, a visit and a practice duel in 3D. 30 fps, as on the Switch (frame time p50 33.3 ms, p95 34.5 ms; GPU 5–7 ms a frame on the Mac in the duel), sound; about 2 GB resident in the council and 2.5 GB in the duel, flat on a screen left idle. The prewarm list covers that route. - Engine: Koei Tecmo's KTGL on NVN, SDK 17.5.3 (Ace Attorney Investigations Collection's SDK);
subsdk0is the movie module (WebM logo and story movies). No virtual address memory:nninitStartupsets a 3.13 GB heap in the 4 GB layout,mem_used80 MB for the rewritten modules. - Files: a newer RDB than Age of Calamity's:
0002/root.rdbandsystem.rdbindex 296.fdatafiles (aPDRKheader, thenIDRKentries) and0002/data/*.file, every entry LZ4 or zlib in 16 KB chunks of a new form (01); scenarios, messages, movies and their subtitles sit in loose folders beside it. The update is a multi-program patch: it carries a whole second program (0100C2A01A730001) beside the game's patch, whichmainstarts withnn::oe::ExecuteProgram(1);nx_extractused to take that one and failed (01). The host runs program 0 only. - Shaders: 59,363 GLSLC outputs (52,691 pixel, 5,359 vertex, 1,311 compute, 2 geometry): 59,214 in the G2S (
_S2G5000/6000/7000) and_DOK0000containers of the RDB entries, read byshader_split.py(idrk_data), and 149 inmain. The vertex shaders are single-stage programs, paired with a cover pixel shader. All build for the Mac (1 h 19 min on a 12-core M6). Three shaders get textures of another type than they declare on some slots (texture type mismatch … bound as is, once each: a 2D array where a pixel shader declares 3D, cube and 3D textures where two vertex shaders declare 2D); nothing in the scenes above looks wrong. - Sound: SDK 17.5.3's
OpenAudioRendererwith an event calls the form without one through the PLT, which the host's hook took for a second renderer; the inner call now goes straight to the SDK (03, general). - Display: the game draws 1280x720 into its 1920x1080 window textures and crops (
WindowCrop), docked as well; a screenshot withoutDM_NVN_SHOT_OUTshows the whole texture, the picture in its top-left corner. - Input: controller only, with the 3D pad layout (the dialogue boxes' auto and log buttons are the right stick and R3).
maininitializes the touch screen and gestures but never reads them; it reads a USB mouse (GetMouseStates), which the host does not provide. Touches as a mouse would suit it, but OCTOPATH TRAVELER 0, Tears of the Kingdom and Unicorn Overlord also read a mouse and no touch screen, so such a rule needs testing on them first. - Mac input: the work folder's
run/ctl.shdrives a run started withDM_CTL. From a fresh save: A (skips the intro), A at the title, A (New Game), A on Recommended, A ×3, the quiz's first answers (A, A, A), A on Ruler, X at Confirm Settings, A ×3 and X, then X at the story's Skip dialog: the council of 195 April, about 2 minutes.run/save_refholds a manual save there: A, A, Down, A (Load), A, X loads it.
Taiko Risshiden V DX (0100346017304000)
- Status: brought up on the Mac from update 1.1.1 (
v131072); playable on an iPhone (IPA 3.3 GB). On the Mac: the notice, the logos and the opening movie, the title, a new game (1560, Kinoshita Tokichiro) through the opening story, the town, the council with a battle proposal, the territory map, a rest at home, a save and a load, and the field battle tutorial, docked with buttons and in handheld mode by touch. 60 fps (frame time p50 16.66 ms, p95 16.85 ms in the town), sound; about 0.7 GB resident and 1.6 GB of Metal allocations, flat over 15 minutes. No red line on that route. The prewarm list covers it (18 lines). - Engine: Koei Tecmo's KTGL on NVN, SDK 12.3.3 (no other game's
sdkbuild);subsdk0is the movie module (WebM opening and ending). No virtual address memory:nninitStartupsets a 3.14 GB heap in the 4 GB layout; the default 64 MB of used memory fits its small modules (main15 MB). - Files: loose files, 3.3 GB: KTGL G1T textures (some in MRLK packs), the game's own TR5 / TE5 / TS5 data, KTSS Opus streams (
.kno, throughnn::codec) and KTSL wave banks. Japanese, Simplified and Traditional Chinese; no English, so the NSP script packs Japanese (DM_LANGinDocuments/env.txtswitches). - Shaders: 212 GLSLC outputs: 45 in
mainand 167 in the G2S containereff/KTGLEffect.g1s(_S2G3000, as Age of Calamity's), read by the existing splitter; the game is almost all 2D sprites (two draws a frame in the menus). - Host changes, all general:
oe::SetCpuOverclockEnabled(the first boot's crash, apm), and gestures forhid::GetGestureStates, recognized from the touch points (03). - Frame rate: KTGL's frame end sleeps in 2 ms rounds until 1/60 s has passed since the frame started, which ended at 18.2 ms (55 fps in the menus and the town); a build cut skips that wait and the presents set the pace (its
patches.py, 03). - Input:
op_mode handheldandpad_layout touch: handheld, the Switch build plays by touch (tap the cards and buttons, drag or flick the map), which on the iPhone are all the touches in the Tap layout, on the Mac the mouse; the layout button brings back the gamepad (the map scrolls with the right stick in the 3D layout; L / R and ZL / ZR switch tabs and lists). It draws 1920x1080 in both modes. - The town's top-right panel runs off the screen on a council day (its "council today" text is longer than the usual "n days left"): the panel starts where the manual's screenshot has it and widens for the longer text; on other days it fits. Not a host bug.
- Mac input: the work folder's
run/ctl.shdrives a run started withDM_CTL. From an empty save: A skips the notice and the movie, A at the title and on New Game, then A five times (the scenario, its story, Tokichiro, his abilities, the settings) and about 150 presses of A through the story to the town (on the council day: A, Right ×3, A enters the castle).run/save_refholds a save in the town on 1560 May 1. Handheld (DM_OPMODE=0), the same by touch: five taps at the centre (T0.5x0.5) reach the load screen of Continue, thenT0.5x0.29andT0.5x0.47load slot 1.
Super Smash Bros. Ultimate (01006A800016E000)
- Status: plays on the iPhone (IPA 17.5 GB) from update 13.0.5 (
v2031616) with its 99 DLC licences. On the Mac: the opening movie, the title, the first launch's DLC unlocks and Challenges board, the main menu, the Smash rulesets, stage select, fighter select, 2-player battles (Frigate Orpheon, Big Blue, Great Bay, Find Mii) at 60 fps with about 2.7 ms of GPU per frame and 2 GB resident (1.2 GB of Metal allocations), quitting a battle to its results and a second battle whose fighters' NROs the game loaded again. - Engine: Bandai Namco / Sora's engine (
app,lib,phx,nu) on SDK 8.2.1, the samesdkand movie (subsdk0) builds as Catherine: Full Body. Everything is in onedata.arc(17.6 GB) beside 14 NRRs;dm/gfx/tools/smash_arc.pyreads its current files (01). - Fighter code at run time: the fighter, item and common scripts are 98 NRO modules in
data.arc(193 MB) that the game loads withnn::ro::LoadModuleas fighters appear (common and item at boot, the opening movie's fighters, each battle's) and unloads after;mainimports their 1,045lua2cpp::create_agent_*functions. They are collected, rewritten and loaded as described under "NRO modules" above (03); 87 need no rewrite at all, 11 have onex18site each. On iOS they are packed into three sections of the executable (gen_embed.py), 157 MB of code. - Memory: the engine reserves 62 GB of address space for all its memory at a static constructor; cuts on
mainmake it 4 GB and on thissdkbuild drop the SDK's 1 GB node area and 1 GB alignment, so it starts 64 MB into the 4.375 GB alias region of the 5 GB block (03). A battle commits up to 2.5 GB of it (DM_ALIAS_WINDOWS). - Shaders: 35,012 (22,197 vertex, 12,761 pixel, 53 compute, 1 geometry): GLSLC outputs in the 41 SSBH shader databases (
.nushdb, 7,764), BNSH in the effects (EFFN, 26,419) and UI layouts (SARC, 752), and 77 inmain. The split readsdata.arc's current files (shader_split.pysmash_arc_jobs). - Mac input: the work folder's
run/ctl.shdrives a run started withDM_CTL(run/step.sh <log> <shot> <frames> <presses…>presses, waits on the frame counter and takes a shot). Fromrun/save_ref(past the first launch): A skips the movie, A at the title reaches the main menu, A (Smash), A (a ruleset), A (Random stage), then at the fighter select (after its "Joined!" banner)LUp~0.85,LLeft~0.85,LDown~0.3,LRight~0.4lands on Mario, A, Plus.
Bayonetta 3 (01004A4010FEA000)
- Status: plays on the iPhone (IPA 14.2 GB) from update 1.2.0 (
v131072). On the Mac: the boot, the title, a new game (difficulty, Naive Angel Mode, brightness, the auto-save notice), the VP9 opening movie, the prologue's first real-time scene and a minute of play in it, with sound. 60 fps in play (frame time p50 16.67 ms, p95 17.5 ms; GPU about 5 ms a frame on an M6), 30 where the game asks for it (the title and loading screens); about 2 GB resident and 3.1 GB of Metal allocations, flat after the first scene. The title is up about 15 s after launch with a warm shader cache. The prewarm list covers that route (678 lines). Dotted patches on some surfaces (a grid every 4 pixels: the platforms breaking up in the prologue, a floor near the camera) look like the engine's dithered dissolve; not compared with the Switch yet. - Engine: PlatinumGames' own engine on NVN, SDK 13.3.2 (no other game's
sdkbuild);subsdk0is the movie module (WebM movies, VP9); Wwise sound throughAudioOut. No virtual address memory:nninitStartupsets a 3.09 GB heap in the 4 GB layout,mem_used116 MB for the rewritten modules. - Files: 72 PKZ archives (14 GB) of DAT archives, compressed per entry with Oodle Kraken or stored, beside 478 loose WebM movies (01).
mainreads them only through Oodle's streaming decoder, so the Mac host's Oodle tool mode now finds and drives that decoder when amainhas noOodleLZ_Decompress(hle_oodle.c);profile_guess.pyturns it on for any game with Kraken PKZ archives (oodle_packs). - Shaders: 6,424 distinct PlatinumGames shader objects (VSO / PSO / CSO / HSO / DSO) in
root_000.pkz(shader_split.pypkz_entries,pgdat_members,pg_shader; single-stage programs). All but two domain shaders build (04). - Host changes, all general: the newest GPU code version the NVN layer reports now follows the SDK (1.16 from SDK 13): the engine refused its built-in shaders at 1.14 and
nnMainreturned at once (03);nvnWindowGetCropandnvnSamplerBuilderGetLodClamp. - Input: the 3D pad layout (the camera is on the right stick).
- Mac input: the work folder's
run/ctl.shdrives a run started withDM_CTL(CTL=<file>picks another run's control file),run/step.sh <shot> <presses…>presses and takes a screenshot,run/snapseq.shboots and takes one every few seconds, andrun/route.sh <run>boots on a fresh save and drives the route above: A at the title, A through the logo, A (Standard), A, B (closes the Naive Angel Mode help), A (Off), A (brightness), A (the auto-save notice), then Plus, Down, A skips the opening movie. Bayonetta (010076F0049A2000) - Status: plays on the iPhone (IPA 8.8 GB) from update 1.1 (
v65536). On the Mac from a fresh save: the boot, the opening movie (USM; Plus skips it), the title and its attract demo, a new game (PLAY, NEW GAME, the auto-save notice, Normal), the prologue on the falling clock tower, the graveyard cutscene (skipped: Plus, Down, A, then Left, A for Yes), the touch-controls tips, the practice tutorial (punch, kick, combos, evade, Witch Time), the graveyard fight of Verse 1 with Witch Time, its autosave and the next loading screen's practice, with sound and no red line. 60 fps in play (frame time p50 16.65 ms, p95 17.1–17.5 ms; GPU 2–4 ms a frame on an M6), 30 in its cutscenes, where the game sets present interval 2; about 2.45 GB resident and 1.54 GB of Metal allocations, flat over 15 minutes. - Engine: PlatinumGames' own engine in its Wii U form, on NVN, SDK 3.6.0, as Bayonetta 2: the same
sdkandsubsdk0builds, and no host change. CRI middleware throughout: the file system (CPK), ADX2 sound (se/andbgm/,.acf/.awb) through one 6-channelAudioOut, and Sofdec movies (USM), all decoded by the game's own code. No virtual address memory: a 3.11 GB heap in the 4 GB layout,mem_used102 MB. - Files: nine CRI CPK archives in the RomFS root (
data00.cpk…data03.cpk,data10.cpk…data14.cpk, 4.25 GB) of PlatinumGames DAT archives, beside the movies and the sound;cpk.pyreads them as they are. - Shaders: 458 BNSH files of one stage each (236 pixel, 222 vertex), the
.vso/.psomembers ofshader.datindata00.cpk; the split (cpk_jobs) takes about 1.7 minutes. All build. - Input: the 3D pad layout (the camera is on the right stick). The touch controls are handheld only; the profile keeps it docked.
- Mac input: the work folder's
run/see.sh <run> [secs]boots on a fresh save (orSV=<save>), pressesPRESSonce frameATis logged and takes a shot everyEVERYs;run/ctl.sh,run/step.sh,run/watch.shandrun/fight.shas for Bayonetta 2;run/tut.shworks through the practice tutorial's pages (Minus is Next), andrun/witch.pytimes the Witch Time page's countdown from shots and dodges at a sweep of offsets (the page wants a dodge right before the strike; it took 20–30 tries).run/save_refis the save at the prologue's chapter screen,run/save_ref2the checkpoint after the tutorial (the tutorial comes back after a load),run/save_ref3the one after Verse 1.
Bayonetta 2 (01007960049A0000)
- Status: plays on the iPhone (IPA 12.5 GB) from update 1.2 (
v131072). On the Mac: the boot, the PlatinumGames logo movie (USM), the title, a new game (slot 1, the auto-save notice, Story, Normal), the opening narration (The Legend of Aesir, which cannot be skipped on a first play), the prologue's cutscene (skipped: Plus, Down, A, A), the tutorial (closed: Plus, Down ×4, A), Verse 1 on the jet, Rodin's cutscene and the next fight with an Umbran Climax and a torture attack, with sound. 60 fps in play (frame time p50 16.67 ms, p95 17.5 ms; GPU 2–3.5 ms a frame on an M6), 30 in its cutscenes as the game asks; about 1.6 GB resident and 1.75 GB of Metal allocations. The title is up about 20 s after launch. - Engine: PlatinumGames' own engine in its Wii U form, on NVN, SDK 3.6.0: the oldest SDK so far, and no host change was needed for it (NVN API 53, as for the other SDKs up to 10).
subsdk0is NVN's GLSLC;mainimportsglslcCompilebut did not call it on the route above. Wwise sound through two 6-channelAudioOuts; CRI's file system and Sofdec movies (USM), both decoded by the game's own code. No virtual address memory:nninitStartupsets a 3.11 GB heap in the 4 GB layout,mem_used102 MB for the rewritten modules. - Files: six CRI CPK archives (9.5 GB, 6,091 files, 12 GB of them CRILAYLA-compressed) of PlatinumGames DAT archives, beside the USM movies and the Wwise sound (01;
dm/gfx/tools/cpk.py). - Shaders: 1,856 BNSH files of one stage each (
.vso/.pso) inshader.dat,shader2.datandshadereff.datofdata000.cpk; the split decodes every CPK entry (shader_split.pycpk_jobs, about 2.5 minutes). All build (04). - Host changes, all general: a cleared attachment larger than the render pass is cleared in a pass of its own (the narration in a corner of a black screen, speckled opening scenes), and
Downsampleof a single-sample texture is a filtered scaling copy (its bloom chain) (04). - Input: the 3D pad layout (the camera is on the right stick). The game also offers touch controls in handheld mode; the profile keeps it docked.
- Mac input: the work folder's
run/route.sh <run> [secs]boots on a fresh save and presses A five times from the title (slot 1, the notice, Story, Normal),run/ctl.sh/run/step.shpress and take shots,run/watch.shtakes a series of shots, andrun/fight.shrepeats a simple combo.run/save_refholds the save after Verse 1,run/save_ref2the one after the second fight.
Advance Wars 1+2: Re-Boot Camp (0100300012F2A000)
- Status: plays on the iPhone (IPA 7.1 GB) from the base game 1.0.0, the only version. On the Mac: the boot, 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 the CPU and GPU mostly idle), sound, about 2.5 GB resident and 1.6–1.9 GB of Metal allocations, level over 15 minutes with no red line.
- Engine: Unity 2020.2.1f3 IL2CPP on SDK 10.4.1 (The Great Ace Attorney Chronicles'
sdkand movie module builds), NVN; FMOD sound throughAudioOut; Bink 2 movies (.bk2) decoded by Bink's Unity plugin inmain. The NPDM declares virtual address memory:nninitStartuphands pool − used to StandardAllocator and sets no heap, so thesdkbuild takes the node-area cut and the 1 GB alignment cut (sdk_patches.py), and the profile reports a 2 GB pool in Engage's 5 GB block: with the heap check's 1 GB the game's malloc ran dry at the first loading screen (03). - Shaders: 20,424: the Addressables bundles (
Data/StreamingAssets/aa/Switch), Unity's 11 GLSLC outputs inmain, and Bink's 32 (2 vertex, 22 pixel, 8 compute), whose control sections and code areas lie loose inmain(shader_split.pyraw_shaders, 04); without them every movie drew white. All build. - Input: the 3D pad layout (the map cursor moves with the D-pad).
- Mac input: the work folder's
run/step.sh <shot> <wait> <presses…>drives a run started withDM_CTL. About 22 s into a boot the opening movie plays: A skips it,L+R~0.5opens the main menu (the first time Nell's introduction, A about 25 times, then the file select); A (Campaign), A, A (Advance Wars, the file), A, A, A, A (the mission, Field Training, Troop Orders) start the first battle.
Pokémon Sword (0100ABF008968000)
- Status: plays on the iPhone (IPA 12.4 GB) from update 1.3.2 (
v458752). On the Mac: the first boot's language, photo and name choices, the opening (the Champion's broadcast, the stadium), Postwick (the house, the Bag, Hop's house), the walk to Wedgehurst and Leon's arrival at the station, the evening in Hop's yard, the starter choice (Scorbunny) and the first battle against Hop (Wooloo, then Grookey), the title screen and a save and load, at 30 fps as on the Switch (frame time p50 33.3 ms, p95 35–36 ms; GPU 4–9 ms a frame on an M6; CPU 42% of a core, 0.15 W), with sound (Wwise through a 6-channelAudioOut); resident memory 1.8 GB after the title, 2.4 GB after 17 minutes of new areas, 1.9 GB of Metal allocations. No red line on that route. The prewarm list covers it (3,273 lines). - Engine: Game Freak's engine before Trinity, on NVN, SDK 7.3.2: the same
sdkand movie module (subsdk0) builds as Super Mario Odyssey 1.4.1;subsdk1is GLSLC, whichmainimports but never calls. No virtual address memory:nninitStartupsets a 3.13 GB heap in the 4 GB layout,mem_used76 MB for the rewritten modules. - Files: 7,699 GFPAK archives (7.9 GB, every entry LZ4) under
bin/archivebeside loose message, battle and Wwise files (01). - Shaders: 41,516: the BNSH entries of the GFPAK archives (
shader_split.pygfpak_members), loose BNSH files, two inmain, and Silicon Studio's YEBIS post effects, 3,155 shaders of a.tfxofile in a zlib stream inmain(tfxo_streams, 04). - Host changes, all general: the Poké Ball Plus (
hid::*Palma*: none connected),pdm::QueryApplicationPlayStatistics(never played),err::ShowNetworkServiceError,nvnDeviceGetWindowOriginModeandnvnDeviceGetDepthMode(03); a pixel shader entry point for G-buffers of float and integer targets, cube views counted in cubes, and textures whose shared edge pages are written but whose own bytes are not are no longer re-uploaded (04). - Input: the 3D pad layout (the camera is on the right stick).
- Mac input: the work folder's
run/ctl.shdrives a run started withDM_CTL,run/step.sh <shot> [wait] <presses…>presses and takes a screenshot (headless: a window that loses focus is throttled and its shots never come), andrun/route_out.sh <run>boots a copy ofrun/save_ref(in the bedroom with the Bag) through the title and out of the front door. From a fresh save, A at the language (about frame 600), the photo and the name (DM_KBD) reach the opening;run/save_hopis a save in Hop's kitchen, before the walk to Wedgehurst: from it, down 2.5 s out of the house, left 3.5 s, up 0.5 s, left 5 s, right 0.6 s and up 8 s reach the hill above Wedgehurst, and A presses with the stick up through the station scene lead back to Hop's yard and the starters (the camera turns with each move: walk in short steps onto the court and press A facing the Pokémon).
Pikmin 4 (0100B7C00933A000)
- Status: plays on the iPhone (IPA 10.8 GB) from update 1.1.1 (
v262144). On the Mac: the boot, the title (a VP9 movie behind the logo), the main menu, a new game (the auto-save notice, players, creature activity level), the opening (the shadow-play story, the cutscenes with Olimar and Moss) and the Olimar prologue in play, Continue from a save; 30 fps as on the Switch, Wwise sound, about 2.6–2.8 GB resident and 1.7 GB of Metal allocations, no red line. The title is up about 25 s after launch. The prewarm list covers that route (806 lines). - Engine: Unreal Engine 4.26.2 (
Carrot4) on SDK 10.4.8 (its ownsdkbuild), NVN, PhysX 3.4, Wwise (its ownAudioOutsink, Opus through the host's software decoder), VP9 movies through Unreal's Switch media player and the SDK's movie module (subsdk0). IoStore containers (Carrot4-Switch.utoc/.ucas) beside one pak (Carrot4-Switch.pak, version 11, Zlib, index not encrypted) that holds the configs, the Wwise media and the two shader code archives. - Rewriter: an exclusive window with an
adrpinside and an observation gate on an epilogue, both new to the rewriter, which now handles them for every game (03). - Memory: Unreal's FMallocBinned3 is cut and runs on the host's malloc zone (UE 4.27's 21-slot layout,
hle_uemalloc.c); the engine region is only reserved without-SwitchVirtualAddressSpaceSize=in this build, so its 62 GB default is cut to 4064 MB instead of agame_argscap, and the SDK build takes the node-area and 1 GB alignment cuts, in Engage's 5 GB block (03). - Shaders: 15,727 distinct (16,190 in the two archives, every one an LZ4 block, as DRAGON QUEST VII Reimagined's); all build.
- Host change, general:
nn::nifmrequests stay on hold for 50 ms after submission, as Horizon's asynchronous request does: answered at once, the game's BCAT thread could finish before its creator marked it started, and about one boot in six stopped at the black loading screen for good (03). - Frame rate: 30 fps. The game steps a fixed frame time (
bUseFixedFrameRate=True, Unreal's default of 30) and asks for present interval 2 (rhi.SyncInterval=2), so a faster present rate would speed the game up. - Input: the 3D pad layout (the camera is on the right stick); it plays with the controller, docked.
- Mac input: the work folder's
run/ctlrun.sh <run>starts a run withDM_CTL,run/see.sh <shot> <wait> <presses…>presses and takes a screenshot,run/mash.shpresses A in rounds. The title is up about 25 s into a boot: A (main menu), A (Story), A (the save file), A (Load); from a fresh save A (Story) shows the auto-save notice, then the number of players and the creature activity level start the opening.
Stardew Valley (0100E65002BB8000)
- Status: plays on the iPhone (IPA 0.6 GB) from update 1.6.15.13 (
v1310720). On the Mac: the title, a new farm (character creation with the software keyboard, Skip Intro), the farmhouse, the farm and its tools, the bus stop, Pelican Town by day and night (lamp light), Pierre's shop and a purchase, the game menu, the mail, a night's sleep and its save, Load from the title, and the Co-op menu (Online Communication goes back to the title with no network). 60 fps (frame time p50 16.67 ms, p95 16.8 ms; GPU about 0.6 ms a frame on an M6; CPU 8% of a core, 0.27 W), sound; 0.46 GB resident at the title and 0.93 GB in play, flat over the last 10 of 23 minutes, 0.3 GB of Metal allocations. No red line on that route. The title is up about 20 s after launch. About one frame in 800 takes 24–47 ms (nine in two idle minutes in the farmhouse): game CPU time, with no pipeline build, upload or present in it, while the main thread otherwise sleeps 95% of each frame inSleepThread(sampled), whose host sleep is exact; probably BRUTE's Boehm collector, not checked further. - Engine: MonoGame for the Switch: MonoGame's and the game's C# compiled to C++ by Sickhead's BRUTE into
main(124 MB of code, 230 MB with its data and bss) with a Boehm collector, as Celeste; XACT sound (Content/XACT, 0.48 GB of wave banks) through the audio renderer; SDK 20.5.27, NVN. Onlymainandsdk. It links Pia and NPLN for online co-op and LDN for local co-op. - Files: 3,549 XNB files, 3,535 of them LZ4 and 8 LZX (MonoGame's compressed XNB).
- Shaders: 65 (23 pixel, 42 vertex): 64 in the MonoGame effects in
main(SpriteBatch's and the game's) and one inContent/Effects/ShadowRemove.xnb, an LZ4 XNB thatshader_split.pydecompresses before reading it (xnb_decompress, 04). Nearly everything draws with SpriteBatch: the prewarm list is its two shaders and four pipelines, and with it nothing was built on demand over the route above. - Memory: no system resource (
sys_resource 0), so no virtual address memory: the game sets a 2.96 GB heap (pool − used memory) in the 4 GB layout; the rewritten modules need 218 MB of used memory (mem_used). - Host changes: none in this profile's change. Two found on this game came before it (PR #120): compressed XNB shaders, and the Mac text box, which releases the buttons that opened it (character creation re-opened the keyboard after every OK) and stays up while the text is shorter than the minimum length.
- Input: the pad only (
mainimports no touch screen or mouse): the D-pad moves between menu items, the right stick moves the cursor, ZL / ZR change the toolbar slot (empty slots are skipped), X opens the menu, Y uses the tool, A checks. The 3D pad layout. The game drops presses while a menu fades in or out (about a second after it opens or closes). - Mac input: the work folder's
run_v/boot.sh <log> [secs] VAR=…runsbuild/valleyhost(SV=<save dir>for a copy ofrun_v/save_ref, the morning of day 2 in the farmhouse),run_v/ctl.shdrives a run started withDM_CTL,run_v/step.sh <shot> [wait] <presses…>presses and takes a screenshot. The title is up about 20 s into a boot: A (Load) and, a second later, A (the save). Menu presses held 0.3 s or more repeat, so hold 0.2 s; walking takes the left stick (LLeft~1.3is about five tiles). In character creation withDM_KBD=<text>, A on each of the three text boxes fills it, and Skip Intro is the last item above OK. Out of the farmhouse (its door is below the gift box), the path east above the weeds, past the mailbox, leads to the bus stop, and east from there to Pelican Town.
BALL x PIT (010086A022444000)
- Status: plays on the iPhone (IPA 1.1 GB) from update 1.293 (
v393216). On the Mac: the boot, the title, a new game, the intro and the descent into the pit, the first dive and its tutorial (moving, aiming and shooting the balls, gems and XP, the enemy waves that overrun the player and end it), and the base-building tutorial (the sidebar, the build menu, placing a wheat field and a forest, the first harvest with ZR), with FMOD sound. 60 fps (frame time p50 16.66 ms, p95 about 16.8 ms; GPU 1–5 ms a frame), about 1.6 GB resident and 0.7 GB of Metal allocations, flat over 10 minutes; no red line on that route. The title is up within 25 s of launch. The prewarm list covers that route (297 lines). - Engine: Unity 6000.0.62f1 IL2CPP with FMOD (one stereo AudioOut, banks in
StreamingAssets) and Addressables (11 bundles inStreamingAssets/aa/Switch) on SDK 20.5.19, its ownsdkbuild;subsdk0is the SDK's movie library. It draws 1280x720 into 1920x1080 window textures and crops the window to 1280x720 (nvnWindowSetCrop), which the present scales to the screen. - Shaders: 38,099 (18,913 vertex, 18,917 pixel, 116 compute, 76 hull and domain, one geometry), 11 of them the player's own GLSLC outputs in
main. The loose serialized files (resources.assets,sharedassets1.assets, the built-in resources) have no type trees; they borrow theShadertree that 10 of the bundles carry (the same type hash), sounityfs.pyneeded no layout, as for Silksong. - Memory: as Hollow Knight: Silksong: 16 MB of system resource, no heap, Unity 6 reserving and committing its memory itself, Engage's 5 GB block, and the node-area cut on this
sdkbuild (sdk_patches.py; its AddressRegionManager is the SDK 20.5.22 build's at the same offsets). The alias region reaches +2.00 GB, 1.26 GB mapped. - Host changes: none.
- Input: docked with the 3D pad layout (
pad_layout full): in a dive the left stick moves, the right stick aims, ZR shoots and X toggles autofire; in the base the left stick moves the build cursor, A confirms, B goes back, X opens the sidebar, ZR sends a worker out to harvest and ZL + R pans. - Mac input: the work folder's
run/ctlrun.sh <run>starts a run withDM_CTL,run/see.sh <shot> <wait> <presses…>presses and takes a screenshot,run/route.sh <prefix>plays the route above from a fresh save (start the run withSV=set to an empty save folder).
Mario vs. Donkey Kong (0100B99019412000)
- Status: plays on the iPhone (IPA 1.6 GB) from update 1.0.1 (
v65536) over the cartridge base game. On the Mac: the opening movie with its sound, the title (Press L+R), file select, the play style choice, the first cutscene, level 1-1 (both colour switches, the spring, a time-out and its restart), the pause menu, leaving the level, level select and the save it shows in file select. 60 fps (frame interval p99 about 17 ms), sound, about 1 GB resident in the level and flat while it runs (0.95 GB of Metal allocations), CPU about 30% of a core; no red line on that route. - Engine: Nintendo's sead / agl / gsys engine (
ainbAI scripts,bgymlparameters) on SDK 15.3.0 with NVN. Its cutscenes are MP4 files (Movie/*.mp4, H.264 1080p and AAC stereo) played through the SDK's movie library (subsdk0:movie::Extractorandmovie::Decoder), their sound through an AudioOut of the game's own. No virtual address memory: the game sets a 3.14 GB heap in the 4 GB layout. - Files: 3,162 RomFS files (1.26 GB), nearly all zstd (
.zs), as Wonder's. - Shaders: 12,691 BNSH / BAHS programs (8,053 pixel, 4,587 vertex, 51 compute): the models' own shader archives in
Model/(8,390), agl's inLib/(3,520), effects,Shader/, layouts and 40 inmain. All translate. - Host changes: two in the movie decoder (
hle_movie.c). The game's movie player keeps the outputs it holds in a ring of 32 per track and pushes with no bounds check, so the host's 64 audio outputs overran it into the heap a second into the opening movie: AAC decoders now offer 32 (Vorbis keeps 64). The player also sizes its AudioOut buffers by the audio decoder'sGetOutputBufferSize, appends each whole and advances its clock by that size, so the host's 128 KB answer made every 4 KB AAC frame play as 0.68 s and the movie froze: an AAC decoder now reports one frame (1024 samples per channel, 2048 with SBR). And advanced blend modes: the level sets SRC_OVER on one blend state, and the Porter-Duff modes now blend with the fixed-function factors they amount to (03, 04).DM_PROBE_POLLtakes a value filter (=<value>) for a word that changes often before the bad store (05). - Graphics:
render_cond on: the game skips clears under render conditions, as Tears of the Kingdom and Animal Crossing: New Horizons do; without it every level was black behind its HUD. - Prewarm list: recorded from a fresh save over the route above (550 lines); with it nothing was built on demand on that route.
- Input: the pad only (
mainimports no touch screen); the default 2D gamepad layout. The title asks for L+R. - Mac input: the work folder's
run/boot.sh <log> [secs] VAR=…runsbuild/konghost,run/ctl.sh/run/step.shdrive a run started withDM_CTL. The opening movie plays at every boot and Plus skips it (about 18 s in); then L+R on the title, A for file 1 (level select) and A for level 1-1, which starts about 15 s later. In level 1-1LRight~0.3,LRight+A~0.25hits the blue switch andLRight~0.55,LRight+A~0.15the red one.
Tools (dm/tools)
| Tool | What it does |
|---|---|
nx_extract/ |
NSP → ExeFS / RomFS / icon with LibHac, merging an update over the base game (C#, dotnet build). --manifest writes ExeFS and a RomFS file list only, for surveys |
nso_fnmap.py |
Carries offsets from one build to another (function matching with addresses masked; globals via their references) |
nso2elf.py |
Converts NSO to ELF for Ghidra / REA |
insn_census.py |
Counts the instruction sites the rewriter must handle |
nso_code.py |
Function ranges from .eh_frame and the data blocks in a text segment, which the rewriter and the census leave out |
nso_syms.py |
Compares the SDK import surface of two games, or of one game (- for the reference) with the host hooks |
nro_collect.py |
The NRO modules a game registers in its NRRs, found as loose files, among an indexed archive's current files (data.arc) or by a scan, written out for the rewriter with nro.tsv; online SDKs are listed as refused |
nvn_slots_got.py |
Builds the NVN fetch table and call sites (GOT-indirect loaders) |
loader_census.py |
Which of the functions a name loader fetches (glad's GL loader over eglGetProcAddress, NVN's C proc loader) the code really calls: the GOT slots or globals the loader stores, loaded by other code (followed through registers, relocated GOT entries and csel, for LÖVE's glad) |