Integrate PRs #75, #90 and #91 with focused correctness fixes:
- Prioritize demanded render pipelines and bound speculative compilation.
- Follow copied jump-table bases and reject W/X register clobbers.
- Collect and rewrite registered NROs, load them on the Mac, and preserve
runtime activation order through unload/reload and symbol rebinding.
- Preserve refused-only manifests and invalidate collection caches when
archive readers or symbol parsing change.
Add pipeline scheduling, register tracing, NRO packaging and loader
lifecycle regression coverage. Document that executable NRO embedding
on iOS remains outside this integration.
Validation: 41 Python tests; 48 pipeline scheduling, 102 NVN and 95
geometry checks; three pipeline-key serialization cases; full host test
suite; Mac host build; iOS loader syntax checks; git diff --check.
Gameplay and controlled performance A/B measurements were not run.
Co-authored-by: blindlight86 <25404088+blindlight86@users.noreply.github.com>
Co-authored-by: v0lumehi <aljic.zijad@gmail.com>
Preserve per-player motor state and SDK vibration handles, apply batches atomically, and route Core Haptics output through existing controller assignments. Add independent launcher controls and lifecycle tests.
Supersedes #86. Physical feel and controller compatibility still require device testing.
Since 626e6a6 a pixel shader whose outputs look like integer writes gets a second entry point, main0_u, appended
after main0. dmsh_fetch.variant, which makes the framebuffer-fetch variant (_bf), rewrote samples to fb0..fb7 in
the whole source, main0_u included, but declared them only on main0, and renamed main0_out's members, which
main0_u still reads: every variant of such a shader failed to compile (use of undeclared identifier 'fb0'), and
the shader lost the tile read (its render passes are not merged). The host takes main0_u from the shader's own
library (nvn_mtl.m entry_u), never from the variant, so the variant is now made from the source cut at
struct main0_out_u, which dmsh_build.py always appends with main0_u at the end. A main0 with no parameters
also got its fb parameters with a leading comma (main0(, float4 fb0 ...)); output-mapping-only variants only.
Checked on Breath of the Wild's library MSL (chain 534b10d1d578, 10,560 pixel shaders), old and new variant()
side by side: the 8,659 shaders without main0_u give byte-identical variants, with and without
output-mapping-only; all 1,639 changed variants of shaders with main0_u failed to compile before and compile now
(Mac), and 1,897 of 1,897 output-mapping-only ones when forced for every key (30 of them the leading comma; no
profile's bf_want.tsv lists a key today); 40 of them compiled for iOS too. A full Mac dmsh_build.py run on the
new chain 657c42552d82: 19,592 of 19,592 shaders and 6,254 variants with no compile failure (main's chain would
lose the 1,639), stamped, both tables made; the Mac host booted Breath of the Wild with it past frame 5,100, its
title presses into play, with no red line. dm/tools/tests pass. dmsh_fetch.py is part of the chain, so
dm/gfx/nvn/dmsh_chain.h is written again and every game's shaders rebuild at their next build.
Co-authored-by: v0lumehi <34629096+v0lumehi@users.noreply.github.com>
Every GameController fed the same input path (front_mac.m), so the last one moved won, and hle_hid.c reported
exactly one npad whatever the game supports. Now the first controller is player 1, with the keyboard, the next
player 2 and so on up to 8 (pad_slots.h; a removed controller frees its player and gets it back when a
controller of the same name reconnects). Player 1 stays on No1 docked and Handheld in handheld mode, player n is
on No n (No1 stays empty in handheld mode: games take Handheld and No1 both for player 1). The game's supported
npad ids decide how many play: a one-player game ({No1, Handheld}) gets every controller as player 1, a
controller past a game's last player is not used, as on the Switch. A connect or disconnect signals that npad's
style-set event through hle_signal_bound_system_event (hle_thread.c), which signals only an event still bound,
from the macOS main thread where the notifications arrive; player LEDs follow the Switch's patterns; the
controller-support applet reports the connected players, capped at the game's maximum; only player 1 shakes.
DM_PAD_MERGE=1 merges every controller into player 1 as before; DM_KBD_PLAYER=own makes the keyboard player 1 on
its own. One controller, or the keyboard alone, behaves as before. iOS is unchanged: DmGamepad.m still feeds
player 1 and never reports further players.
Rebased onto a678703 (front_mac.m's includes, the Input row of docs/development.md). Checked: host unit tests
pass with the new t_hid (44 checks: slots and reconnects, npads docked and handheld, one- and four-player games,
events, LEDs, the applet); the Mac host builds for Breath of the Wild and Mario Kart 8 Deluxe with no new warning;
hle_hid.c and hle_thread.c pass an iOS SDK syntax check; Hades in a window with the keyboard and DM_KEYS reads
the supported ids (0x10f), and its scripted presses take it past the title. Two controllers were not run here;
the PR's author played Mario Kart 8 Deluxe in 2-player split screen with an Xbox Wireless Controller and a
DUALSHOCK 4, each its own kart, and a controller disconnected and reconnected during play got its slot back.
Co-authored-by: v0lumehi <34629096+v0lumehi@users.noreply.github.com>
A development option; the Mac is the development host, not a platform for players. The same steps up to the
shaders (extraction, DLC, rewrite, run-time captures and decodes, heap check, shader split), then only the
Mac's: dmsh_build.py without IOSOUT (an inherited one is dropped), a Vulkan or GL game's library without --ios,
the Mac host for the profile into <id>_build/tools/dmhost (build_mac_host, through DM_GAMES_DIR /
DM_GAMES_FIRST for a provisional or derived profile) and the button-icon font into <id>_build/font. No iOS
shaders, Xcode project or IPA; the extracted files are kept whatever KEEP_EXTRACTED says, and the last line is
the command that runs the game (its save in <id>_build/run/save, DM_NVN_SHMAP for an NVN game, DM_AOC_DIR
with DLC, the --env-file switches). It refuses --prepare-xcode and an Intel Mac.
Reuse: the Mac libraries have a key of their own (shader/mac-b-<chain>.key beside the cache folder,
shader/vk.key / gl.key), which an iOS build writes too, so a Mac-only build after an IPA reuses its shaders;
either key only while the Mac libraries carry a full build's stamp (inputs.tsv). An iOS build after a Mac-only
one translates again (dmsh_build.py's per-shader keys include the iOS compile convention) and takes the Mac
libraries from the Metal compile cache. build_mac_host now has a reuse key too (the compile line, the sources and
headers of the folders it compiles, the generated profile header, libvpx, the third-party headers, clang and the
SDK), which also saves the rebuild for the Oodle / LZ5 decodes and the run-time captures. build_vk_shaders and
build_gl_shaders share layer_shaders. The default (ios) path builds as before.
Checked: dm/tools/tests 34 of 34 (11 in the new test_nsp_mac.py); Hades in Mac mode ran the Mac stages only,
printed the run command and exited 0, and a second run reused every stage, the host included (2 s); the host it
built boots Hades from that command. Breath of the Wild (NVN, with DLC) in Mac mode restored its 19,592 shaders
from the cache, printed a command with DM_AOC_DIR and DM_NVN_SHMAP, and the host booted it from that command into
play. Not run end to end here: a GL, provisional or derived game in Mac mode, and an IPA after a Mac-only build
(unit tests with the tools stubbed).
Co-authored-by: v0lumehi <34629096+v0lumehi@users.noreply.github.com>
60 fps on a 144 Hz screen cannot be paced evenly: the display link counts a tick on the refresh nearest each 1/60 s
slot (link_range's virtual ticks), so frames stay up two and three refreshes in turn (13.9 / 20.8 ms) and Apple's HUD
shows the frame interval jumping between them. link_sync now tells the front end the link's rate (60, 30, or 120
with DM_PRESENT_LINK=2; front_screen_match), and in fullscreen front_mac.m switches a screen whose mode is not a
multiple of it to the mode of the same size with the highest rate that is, with kCGConfigureForAppOnly, so macOS
switches it back when the host quits; leaving fullscreen or that screen switches it back at once. A window keeps
the screen's rate and the virtual ticks: the switch changes the whole screen for every app and most external
screens go black for a second or more. DM_SCREEN_MATCH=0 keeps the rate in fullscreen too.
Changes to the PR: the switch only in fullscreen, with the way back on leaving it (the PR switched in a window
too); no switch without a window (the windowless host's main thread runs the guest); the rate to match is
atomic; the link takes a rate change only once its object exists, so one made while its thread starts is not lost,
and sets the range directly instead of logging a pacing mode -1; the screen this host switched takes its rate
from its mode, which maximumFramesPerSecond may not show yet. The link now also sets its range again when the
window moves to another screen, which kept the first screen's range before.
Checked on this Mac's built-in 120 Hz screen (a multiple of every link rate) with a harness that drives
front_mac.m asked for 50 fps: no switch in the window, 120 → 50 Hz in fullscreen, 120 Hz again on leaving it, and
120 Hz after quitting in fullscreen; at 60 fps and with DM_SCREEN_MATCH=0 nothing switched. Hades windowed and
fullscreen (DM_FULLSCREEN=1) paced at 60 as before, the unchanged host alike. nvn_mtl.m passes an iOS SDK
syntax check. The link's new range after a switch has not run here (it needs a 144 or 165 Hz screen); the PR's
author measured the windowed switch on a 144 Hz XB273U at 2560x1440: 120 Hz, the HUD's frame interval steady at
16.7 ms, 144 Hz again after quitting.
Co-authored-by: v0lumehi <34629096+v0lumehi@users.noreply.github.com>
- Cause: Galaxy 2's GX vertex lighting (Hagi's SPIR-V) normalizes the light directions the game leaves at (0, 0, 0) for its characters' key and fill lights and takes fast::max(0.0, dot(ldir, NaN)) as the spot term. fast:: leaves a NaN operand's result to the GPU: the Switch's and the M4 Pro's return 0, so the attenuation is 1; the A20 Pro's did not, and the half of Mario that the fill light faces came out white (the fill light sits at the camera's left in view space, so its terminator runs down the middle of a character facing the camera). Found on the Mac by dumping the light uniforms of the character draws (every one has both directions at zero) and a Metal test of the expression on the M4 Pro.
- vk_shaders.py, gl_shaders.py and dmsh_build.py (precise): spirv-cross's fast::min / max / clamp are written as precise:: (IEEE fmin / fmax, which give Maxwell's FMNMX and saturation results); the iOS Vulkan and GL libraries are compiled with -fmetal-math-mode=relaxed, as the NVN chain's are. Plain or fast:: min / max / clamp compile to the fast_ intrinsics in every Metal math mode, so relaxed alone does not do it. The NVN chain had the same calls, unguarded by its isnan selects (Paper Mario: The Thousand-Year Door: 124,000 fast::max, 98,000 fast::clamp, 41,000 fast::min).
- dmsh_geometry.py capture: clip distances as spirv-cross writes them (gl_ClipDistance [[clip_distance]] [8], and gl_ClipDistance_N [[user(clipN)]] for the pixel stage); since bb585cd the shader build stopped for every game with such vertex shaders (Luigi's Mansion 3, Monster Hunter Rise). Checked against all 1,423 distinct vertex output lines of the games' libraries on this Mac.
- Shader chain: every profile's dmsh_chain.h rewritten, Mario Kart 8 Deluxe's included (bb585cd had left 45 of the 46 before it on the version before it, which dmsh_build.py refuses, so the NSP script's iOS shader step stopped for those games). Rebuilt for the Mac and iOS from shader packages checked record by record against the profiles' maps: Luigi's Mansion 3 and Monster Hunter Rise on this chain; Paper Mario: The Thousand-Year Door, 13 Sentinels, ANIMAL WELL, FINAL FANTASY TACTICS, Raging Loop, Metroid Dread, Three Houses, DARK SOULS: REMASTERED, Cuphead and Fire Emblem Engage on the one before the capture fix (none of their vertex shaders writes clip distances). Every shader builds but Monster Hunter Rise's 2 (55 before). Their c1_full / cb_extent lists changed only by rows of geometry shaders built as compute kernels since bb585cd (Monster Hunter Rise's cb_extent and the other profiles' lists carried over, each header line naming what was checked).
- Verified: Galaxy 2 on an iPhone (A20 Pro), Mario lit as on the Switch; Galaxy 2 on the Mac along the storybook route, same red lines; Paper Mario on the Mac with fast:: and precise:: libraries from the same MSL: same pictures and red line, GPU time p50 6.73 / 7.00 ms and p95 9.77 / 9.09 ms (the Mac was busy with shader builds during both)
Capture vertex outputs, run geometry shaders as compute, and rasterize compacted primitives through a general NVN pipeline. Rebuild the pinned translator and AI shader chain, add geometry regression coverage, and document supported behavior and remaining limits.
Profile: virtual address memory as Patrick's Parabox (nninitStartup main+0x493300 hands pool − used to StandardAllocator), Ace Attorney Investigations Collection's layout (1 GB pool, 5 GB block), docked, 3D pad layout (the investigation's view is on the right stick), interval 1, 17,069 shaders from the Addressables bundles and Data/Resources (6 geometry shaders, a wireframe effect and Unity's VR blit, do not build), chain 0379eb846acf, a 231-line prewarm list from boot to the first investigation. Measured over 15 minutes (probes on VammManager::AllocateAddressRegion / FreeAddressRegion, the DM_SVC map log): StandardAllocator commits 0.29 GB of its 960 MB, Unity's own regions hold 1.10 GB (1.03 GB mapped), 1.26 GB mapped in all, 1.96 GB resident.
- patch_nso.py: the AddressRegionManager node area cut (1 GB → 62 MB) for this sdk build (sdk+0x37c140 / +0x37c1dc, the constructor at +0x37c130), as for Parabox's SDK 13.3.7 build.
- shader_split.py unity_compute: Unity 2020.3 keys a ComputeShader kernel's variants by keyword set (variantMap) where 2022.3 lists them (uniqueVariants); the split stopped with KeyError 'uniqueVariants' on this game's bundles (147 kernels now). Monster Train 2's tables are unchanged.
- profile_guess.py heap_signals: a heap check with no heap set whose alias region is used up to within 0.25 GB of its end sets a 1 GB pool (Unity 2020 / 2022: StandardAllocator takes pool − used and Unity reserves its own regions beside it). This game's check reached +4.26 GB of 4.38 in 5 s with the hardware pool; with 1 GB it stays at +2.01 GB. dm/games/README.md documents it.
From the PR (The Legend of Zelda: Echoes of Wisdom, 1.1.0, on the Mac):
- A view whose level count runs past the texture's levels gets the levels it has, as deko3d's descriptor writer clips the maximum level (the game views levels 1 + 7 of a seven-level texture, which Metal's validation refuses); a base level past them is an empty slot (red line 109).
- tex_by_id composes the descriptor's swizzle with the format's mapping (nvn_fmt_to_mtl: alpha 1 for RGBX-type formats, red and blue swapped for the RGB565 family), which was computed and never applied: the hearts' RGB565 mask drew as red rectangles.
On top of the PR:
- The red / blue swap is for bits in the guest's order, so it applies only to a texture uploaded from guest memory (an NvPkg in the serial table) or read from it in place (a texture on a pool buffer). What Metal renders or clears into a 16-bit texture is in Metal's order: with the PR, a target cleared to red sampled blue.
- Metal gives a swizzled view only the read usage (a base of usage 0x7, its swizzled view 0x1), and the image binder sent such a slot to the dummy image: with the PR every RGBX8 / RGB565 image lost its writes (red line 32). An image takes no channel mapping, as a Vulkan storage image view must have the identity swizzle, so img_fit gives the slot the same view without one (kept in the slot's image view, as for a class change).
- The on-chip read merge needs the sampled view's swizzle to be plain: a tile read takes no channel mapping, so an RGBX attachment merged into the next pass read its stored alpha.
- Graphics tests: 13 of the 27 had stopped building. Every test that includes nvn_mtl.m now links hle_nvn.c, the large host stack and weak host stubs (t_host_stubs.c) through one Makefile variable, the tests that stubbed prog_key for want of hle_nvn.c drop the stub, t_present_crop / t_mip_extent rename hle_nvn.c's static g_ft (nvn_mtl.m has one too), and the budget test gets DM_GAME.
t_view_swizzle (203 checks) reads the mappings back from the GPU through tex_by_id: the PR's cases, now with uploaded textures for the swap, plus RGB565 / BGR565 targets Metal cleared to red (red, no view) and an RGBX8 image (sampled alpha 1, writes through img_fit's view stored as written); without the upload rule it fails 9. t_view_levels 34 checks. Every graphics test builds; the ones still failing fail the same with main's nvn_mtl.m: t_refl 2 (reflection samples in a local cache), t_reorder_chain 30, t_mip_extent 2, t_rt_range 4 to 8 per mode, t_pfv 1, t_rt_range_ro (a missing JSON file).
Co-authored-by: nocmt <26828323+nocmt@users.noreply.github.com>
The watchdog read the clock before it took g_aos_lock. The return from the background restarts the AudioUnit under that lock and stamps its last callback time (au_go), so a watchdog tick that had read the clock just before waited for the lock and then compared an older time with the newer stamp: the unsigned difference wrapped ("has not called back for 18446744073662 ms", DRAGON QUEST VII Reimagined on an iPhone after 10.7 s in the background) and a running AudioUnit was handed to the silent sink and restarted. The clock is now read after the lock is taken, as every au_go caller does. Mac: 30 s of Luigi's Mansion 3 with real output, no watchdog line
The launcher from v0lumehi's PR #62 (Launcher.swift, SwiftUI: the game's icon, name and version, Play, and the settings), built as their issue #64 proposes: dm/games/settings.tsv declares each player-facing setting once (the environment variable it sets, its kind: choice, range or toggle, its default and the condition under which a game offers it). build_ipa.sh runs dm/games/settings.py, which keeps the rows that hold for the game and fills in its defaults: the host's own from game_profile.h (DM_GAME_FPS_TARGET, DM_GAME_OPMODE), the build's (DM_LANG, DM_SHOW_FPS), the game's languages from its control data, and the render scale only for a build with the rewriter's render-scale stub, which patch_nso.py now records in main's table (RENDER_SCALE: Breath of the Wild 1.9.0, Tears of the Kingdom 1.4.3). The result goes into the app as dm/launcher.json, and the launcher draws one row per setting from it, so a new setting is one table row. Diagnostic switches stay environment-only. Breath of the Wild offers frame rate, render scale, console mode, 15 languages, the frame-rate counter and Apple's performance HUD.
Differences from the PR's launcher:
- The host's loading still starts at app launch. Only the loader read the frame rate and the render scale that early; it now just maps the frame step state page, and dm_fps_state_apply (main.c, when the game starts) writes both, so a boot is no slower with the launcher (the PR started all loading when Play was tapped). The game view, the pad and the frame-rate counter are made after Play, since they read the launcher's switches.
- The language choice applies: main.m sets the packed language as a default before the launcher runs, which the PR then took for a setting from env.txt and kept. main.m now lists the variables an env file set (DM_ENV_FILE_KEYS); only those are kept, and the launcher shows them greyed out.
- No new profile key for render scale (the rewriter's table tells), and the scale stops at 100 % (the above-1080p part is not in main). No Mac launcher.
The choices are kept in UserDefaults; DM_LAUNCHER=0 in Documents/env.txt skips the screen, as does an app without launcher.json. README: the settings screen in the Play section; dm/games/README.md and docs/development.md describe the table.
Checked: Launcher.swift type-checks against the iOS SDK and the Breath of the Wild IPA builds with it (launcher.json and RENDER_SCALE inside); the Mac host applies DM_FPS_TARGET=30 from the first present and DM_RENDER_SCALE=0.75 when the game starts. Not yet run on an iPhone.
Co-authored-by: v0lumehi <34629096+v0lumehi@users.noreply.github.com>
Taken from v0lumehi's PR #62, the parts that hold for every game (none is gated by a title, build or profile; all but the Mac front end reach only the NVN games):
- nvn_rec.h: the replay's Metal calls (encoders, draws, state, argument table writes, commits, event signals) go to a worker thread as records, in the same order. The submit thread waits for it at the end of each submission and before anything that waits for the GPU or uses another queue. On by default on iOS, DM_NVN_ENC_THREAD=1 on the Mac.
- Uniform shadows of 512 bytes or more (ubp_get) are copied by a worker; a commit waits for the copies queued before it, a CPU read of a shadow for its own (DM_NVN_UB_COPY_THREAD=0 copies on the submit thread as before).
- Deferred inline uniform updates are kept across render pass ends (DM_NVN_UB_LAZY, at most 160) and land before the next copy, a draw that reads their memory other than through uniform bindings the shadows cover, or the end of the submission, instead of in a copy encoder after every pass.
- A two-level address → pool cache (by 4 KB page, then the last 8 pools found), and the pass dump check answered first when it is off.
- DM_NVN_GPUPROF=f or f1-f2: every encoder's GPU start and duration, from Metal 4 counter heap timestamps.
- Mac: the drawable follows the window in pixels (in fullscreen it had stayed at 1280x720), the process declares latency-critical activity, and DM_METAL_HUD=1 shows Apple's performance HUD (on the iPhone too, from Documents/env.txt).
On top of the PR:
- A dispatch lands the kept updates first when it reads or writes their memory through a storage buffer or its indirect arguments, or reads a part of a uniform binding the shadows cannot give (ubd_conflict_cs, with the per-stage check it shares with draws, ubd_conflict_sh). Dispatches open the copy encoder through blit_open, which never landed them, so they ran before the updates. Transform feedback draws land them first, as every pass end did before.
- A deferred update that a newer one covers whole is dropped from the landing list. Breath of the Wild rewrites the same 64 bytes before every draw group, so the list held up to 160 repeats that every binding lookup scanned: the port cost 16,800 instructions a draw against main's 13,200, and costs 13,500 with the drop.
- The encoding thread records encoder labels (DM_NVN_ENC_LABEL) and counter heap timestamps (DM_NVN_GPUPROF): without them either diagnostic stopped the host on iOS.
- DM_NVN_UBP_CHECK waits for the copy worker before it reads a shadow. It compared unfilled shadows (293,447 "different"); now 232,000 equal and 0 different with the encoding thread on, 234,000 and 0 with it off.
Left out: Breath of the Wild above 60 fps and above 1080p (rewriter cuts for its build and a profile key), the Mac GPU filler and screen mode switch, and the iOS launcher.
Breath of the Wild on the Mac (the Great Plateau save, 60 fps cap, beside main under the same load): submit thread 4.30 against 4.48 ms a frame, GPU p95 6.2 against 7.0-7.3 ms, 1.000 of real time, no red lines; with the encoding thread the submit thread takes 3.16 ms (9,700 instructions a draw). t_nvn 100/100 with the encoding thread off and on; the hosts for TOTK and Hades build.
Co-authored-by: v0lumehi <34629096+v0lumehi@users.noreply.github.com>
- The user confirmed the build on the iPhone
- README: games table row and picture (the hotel lobby); AGENTS.md: the list (forty-three games) and a note on the port; development.md: status
- Files and shaders: Next Level Games' engine keeps every RomFS file but the Wwise banks in archives of its own, <name>.dict indexing zlib chunks in <name>.data; update 1.4.0 ships <name>.patch beside the unchanged base files, a variant bitmask and per-section records that rebuild each section from base and patch bytes (the game's algorithm, main+0x1c8d0 / 0x1cbd0). shader_split.py nlg_shaders reads both: 5,317 shaders (5,149 in the base game), all in init, control sections back to back in one section and their code areas in the same order in another, one NVN program per stage (SINGLE_STAGE '#NLG'); 46 of the first 103 the title uses exist only in the update (docs/agents 01)
- Shader chain, for every game: a texture array over the driver table c2 (Ryujinx's texture array from a buffer, fp_t2das_cb2_0: 586 shadow-cascade pixel shaders indexed by (0x80 + cascade offset) / 8) is named tex_c2a / samp_c2a with its c2 offset (dmsh_build.py c2_array), and the replay fills element k from the slot whose c2 word lies at that offset + 8k (nvn_refl NVN_RB_TEXA / NVN_RB_SAMPA, nvn_mtl.m c2_handle). A pixel shader that writes an output from integer bits, or nothing but constants, gets a second entry point main0_u whose colour outputs are the unsigned vectors of the same bits (dmsh_build.py fs_uint); a pipeline whose written colour attachments are all unsigned integer formats takes it (pso_all_uint). The G-buffer is R32Uint + RGBA8Uint: about 1,400 pipelines failed to build and every scene draw was skipped. Every other profile's dmsh_chain.h rewritten with its c1_full / cb_extent lists carried over (the change touches no c1 or uniform read); Luigi's Mansion 3 built for Mac and iOS
- Host, for every game: the game patches texture handles into its recorded command buffers in place, at the command start + 0x10 it took from GetCommandMemoryUsed before nvnCommandBufferBindTexture (main+0x7c8964; the store at main+0xb7d0d4), so a single texture bind is recorded in the hardware's layout, {stage, slot, handle at +0x10, texture / sampler pool bases} (OP_BIND_TEX1, nvn_obj.h bind_rec decodes it at every replay site); the host's own layout put the patched handle over the bind count, the post passes read stale handles (a stretched icon over the title, a white screen in a new game) and a corrupted record now gets a red line. t_nvn: the handle patched at the command start + 0x10 is the one replayed (102 checks)
- Host, for every game: a depth texture drawn through a colour view of the same pixel size (a D16 1024x1024 texture through an R16 view; Metal has no colour view of a depth texture, and the boot aborted in pipeline validation) draws into a colour shadow, copied in before and back after the pass as a compressed target is (nvn_mtl.m shrt_block, D16 and D32F); nvnCommandBufferSetDebugLabel; nvnBlendStateSetAdvancedMode (red line for a mode other than none); a draw whose program has a geometry shader with no library gets a red line naming it (it is drawn without that stage, with the vertex shader's positions as clip coordinates)
- Frame rate: 60 fps (30 on the Switch). The frame limiter (main+0x7a3e70, a frame callback called with the milliseconds since the frame began, main+0xb7c308) runs jobs or sleeps until 33.333 ms have passed, a float in read-only data (main+0x13c94dc), and the game steps by the time that really passed: 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). hle_fpslock.c gets rows with no pointer (the field at main + offset; the loader leaves read-only data writable, the image's data block on iOS too) and a frame-time kind: 1000 / fps_target, at 60 less 0.5 ms, since the work after the limiter made an exact 16.667 ms 59.6 fps with a frame shown twice every 2.5 s; slightly shorter, the presents set the pace (lobby 60.0 fps, display interval p99 18.69 → 16.67 ms). Profile fps_target 60 with present_interval display (the game also asks for interval 2); DM_FPS_TARGET=30 keeps the game's 33.333 ms
- Not done: the tessellation stages (15 hull, 18 domain shaders: two programs draw twice a frame for about 30 frames, in the title scene behind the file select as it slides out before a new game; skipped, since Metal 4 has no tessellation stage) and 10 geometry shaders (none drawn from the title to the lobby). Vertex shaders that feed them can be found from the shader headers (docs/agents 04)
- Mac: the boot notices, the title, a new game, the opening cutscene (the bus ride, the arrival at the hotel) and the lobby in play at 60 fps, about 3.7 GB phys_footprint (3.0 GB of Metal allocations); red lines at run time: the geometry shader that does not build, the conditional render the host draws anyway, primitive restart 0xff (no 8-bit strip seen). Diagnostics: DM_NVN_TREG_ID=n logs that texture ID's registrations; the pass dump prints the handles of texture binds
- The user confirmed the build on the iPhone
- README: games table row and picture (the first battle: the Archwitch's attack on the soldiers); AGENTS.md: the list (forty-two games) and a note on the port; development.md: status
- Engine: Unreal Engine 5.6.1 (Square Enix, the desktop renderer), the newest Unreal so far; everything in one IoStore container beside one pak; the sdk and subsdk0 builds are STEINS;GATE RE:BOOT's
- Rewriter, for every module: code before the first unwind entry is one function (each site there was a function of one instruction, so a byte jump table at main+0x219234 went unseen), and a function longer than 256 KB is analysed in pieces cut after a ret / br / b that no conditional branch or b within 64 KB crosses, with far branches and jump table targets checked against windows and retry blocks afterwards (this main's 82 MB without unwind entries took 100 GB of memory as one function; now 2.7 GB resident). Checked: DRAGON QUEST VII Reimagined's main byte-identical to its rewrite by the previous patch_nso.py; FINAL FANTASY RESONANCE's, Octopath Traveler 0's and DRAGON QUEST XI S's byte-identical between two different cut placements; only DQ VII's main among the 120 modules of 33 games on hand has other sites before its first unwind entry (none in a window or jump table)
- Shaders: no archive in the pak; the two libraries are IoStore shader library chunks whose tables and shader groups are Oodle-compressed, read through the game's own Oodle in the Mac host's tool mode with no boot (uepak.py Toc / io_archive / OodleHost; library names from CityHash64 of the manifest's archive names) and written as decoded version 2 archives; the NSP script uses them for any game with IoStore shader libraries. The pak's single-block files store block size 0 (Pak.entry). 73,496 unique shaders; 11 do not build (3 geometry, 8 compute with 3D image atomics), none met so far. 2,077 particle vertex shaders pick their texture slot at run time (tex_dyn_slots); CopyStencilToLightingChannelsPS writes integers to an R8Uint target (shader_facts_manual.tsv; without it the sprites were dark silhouettes)
- Host, for every game: a shader with run-time texture handles in a game without tex_dyn_slots freed its reflection twice and aborted the host (nvn_mtl.m); oe::MuteRecording / UnmuteRecording (no-ops)
- Memory: the FMallocBinned3 constructor call is cut and the host's allocator runs Unreal's heap (hle_uemalloc.c layout UE5_6: UE 5.5+ FMalloc has 32 slots, with MallocZeroed and two more TLS cache calls); the SDK's node area is cut to 62 MB. The engine's own page allocator only grows its use of the engine region (2.03 GB of address space over seven minutes, about 1 GB mapped), and a 2 GB region aborted in the first battle, so the region is 4064 MB in Engage's 5 GB block (this sdk build aligns regions to 2 MB, no alignment cut needed)
- Frame rate: 60 fps (30 on the Switch): the host writes rhi.SyncInterval 1 (hle_fpslock.c rows for this build; profile fps_target 60); the game steps by real time (DM_NVN_REGLUM traces at 30 and 60 fps match from the load through a walk)
- Mac: first-boot settings, the title, a new game, the opening cutscene, the first field and its autosave, Continue, and the first two battles at 59.7 fps, 2.3-2.6 GB resident, flat over seven minutes, no red line apart from a few texture type mismatches at one moment of the new game's load in some runs. Docs: docs/agents 01, 03, 04 and development.md
The replay jumped to 0x1000000003, a CALL record's header: a JUMP's target had been overwritten by the next recording. Tears of the Kingdom gives each model's prerecorded lists fixed slots of the size a recording of that list measured with GetCommandMemoryUsed, slot k at base + size × k, and passes AddCommandMemory max(size, 512) (main+0xbb120; its memory callback is empty). The host kept 24 bytes free after every record for "a JUMP or an END", so a 496-byte list's END went to a host segment and the 16-byte JUMP at 488 reached 8 bytes into the next slot, whose first record landed on the JUMP's target.
The host now keeps 16 bytes (one JUMP) after every record but the END, lets the END take a segment's last bytes, and links only recordings that have records. Every other record is at least 16 bytes, so a JUMP only stands where a record was to go. Lists that fit their slot exactly no longer go to host segments either (the 504-byte lists Tears of the Kingdom records at load did). t_nvn "slotted lists" records lists of 64 to 1064 bytes into such slots: on the old recorder the 496-byte one broke.
The host writes Unreal's t.MaxFPS 60 and rhi.SyncInterval 1 into the game's console variables (hle_fpslock.c rows for main f7ec03e4); these are the two limits StevensND's 60 fps mod for the base main lifts with code patches (UEngine::SetMaxFPS returning at once, the name r.VSync broken). Unreal steps the game by real time: on the Mac 59.8 fps, the same route at 30 and 60 matches at time scale 1.00, same resolution, no new red line.
stb_vorbis cut a type-2 residue's interleaved vector at n * 2 values even when its submap holds more channels, so the rest of the packet was read as the wrong data: the 5.1 tracks libvorbis encodes came out distorted and gurgling, with pops. The bound is now n * ch, as in the Vorbis spec and libvorbis's res2_inverse, and the temporary-memory estimate for a caller-owned buffer counts every channel of the stream for type-2 residues (with the decode fix alone, a 6-channel stream overran the advertised buffer). Upstream stb still has the bug (nothings/stb #879, #1594).
Link's Awakening's opening and title movies were the report; DRAGON QUEST XI S's movies open only 6-channel Vorbis tracks too. Its 23 6-channel cutscene and title tracks, decoded as the host does, now match FFmpeg to within 0.02 PCM16 units, where main's decoder was off by an RMS of 1,700 to 70,000 units.
dm/host/tests/t_vorbis builds tiny Vorbis streams in memory (types 0/1/2, mono to 8 channels, two submaps) and checks heap and fixed-buffer decoding through the pull and push APIs, with and without STB_VORBIS_DIVIDES_IN_RESIDUE; it needs no game profile. dmhost rebuilds when stb_vorbis.c changes.
On top of the PR: the test rules sit with the others, and the stb_vorbis README and the per-game notes say what the fix repairs.
Co-authored-by: ohxiaoguang <44696970+ohxiaoguang@users.noreply.github.com>
Each game gets its own project in <NSP folder>/<title ID>_build/xcode, registered in a shared NxNative.xcworkspace; a target phase copies the RomFS, shader libraries and add-on content into the app before Xcode signs it, and preparing again keeps the bundle ID and team chosen in Xcode. The unsigned IPA build is unchanged.
On top of the PR: the resource phase stops when the profile's shader chain is no longer the prepared shaders' (the host loads only its own chain), prepared projects ask for the Increased Memory Limit capability (DM_ENTITLEMENTS overrides), the source links are made again on every preparation, and scheme names keep non-ASCII letters. The PR's TOML launcher is left out.
Co-authored-by: Animax Deng <Animax.deng@gmail.com>
- The user gave the go-ahead after the iPhone build
- README: games table row and picture (Maribel and the hero by the boat at night, the scene the image-handle size query fix brought back); AGENTS.md: the list (forty-one games) and a note on the port; development.md: status
- Files and shaders: Unreal Engine 4.27 with IoStore containers beside one pak (version 11, its footer magic 0xcc bytes from the end, uepak.py magic_back); the 62,901 shaders of the pak's two shader code archives are LZ4 blocks, which uepak.py decodes itself (lz4_shaders), so the NSP script runs no Oodle host when no record is Oodle. One global shader writes integers into an R8Uint target (shader_facts_manual.tsv)
- Shader chain, for every game: a texture query on an image handle (c2 0x120 + 8 r: Unreal's GetDimensions on the RWTexture3D a compute shader writes) had no type in a facts table built without PC shaders, and the 2D default took a 3D image's depth from its width; the translator now translates such a shader again with the image types of its first translation (dmsh_xlate ImageTypesOf; dmsh_xlate.dll rebuilt, dmsh_chain.lock updated). Host, for every game: a texture binding of slot 32-39 reads the image bound in the image slot that shares its c2 word (nvn_mtl.m tex_slot_h; Hades II's texture slot 34 without an image still reads the texture). Unreal's volumetric lightmap indirection patch (7b958a7b21e72801) read the empty texture slot 32's 1x1 size and wrote only cell (0, 0, 0), so in the opening cutscene every character and prop sampled the atlas origin, a daytime level's bricks baked 2^15 brighter (white figures about 10^4 too bright). Monster Hunter Rise has four such queries. Every other profile's dmsh_chain.h rewritten with its c1_full / cb_extent lists carried over (the change touches no c1 or uniform read); DRAGON QUEST VII Reimagined rebuilt for Mac and iOS
- Host, for every game: stencil-mode texture builders (nvnTextureBuilderSetDepthStencilMode STENCIL) read the stencil of their depth-stencil storage (the hero was drawn as a dithered silhouette); the audio renderer at 32 kHz on the CPU path (the SDK makes its upsampler only for DSP rendering: UpsamplerManager::Allocate returns null and the host resamples 3:2); GetAudioDeviceOutputVolume; the shop and Nintendo Account calls of the title (ec Finalize / Cancel / GetErrorCode / ShowShopSubscriptionDetails, GetAuthorizationCode / GetState); kpr::GetKeyCodeMap; the movie output format's nv12-width / nv12-height beside nv12-uv-stride (Sofdec's VP9 movies were grey with bands)
- Memory: MallocBinned3 reserves 37 GB of pools in the engine region; its creation is cut and an observation gate hands the engine the host's malloc zone (patch_nso.py cut + gate OBS_UE_MALLOC, hle_uemalloc.c layout UE4_27; patch_nso.py places a gate where the scratch registers it uses are dead, scratch_dead); the region is 2 GB (game_args) with the SDK's node-area cut on this sdk build, in a 3.75 GB guest block; about 2.6 GB resident on the Mac
- Frame rate: this build's t.MaxFPS and rhi.SyncInterval in hle_fpslock.c; the profile keeps the game's 30 fps
- Diagnostics: DM_CTL (Mac) drives a running game line by line: presses, shots, pass and texture dumps, texlist (textures of a target with storage and block-linear layout), memdump, peek, Unreal console variables (cvarf / cvari), env; the pass dump prints buffer-to-texture copies and buffer textures' format, size and storage, and DM_NVN_CBDUMP saves the source bytes of 3D uploads (docs/agents 04, 05)
- Mac: the logos and opening movie, the title, a new adventure log, the opening cutscene (the well, the night scene with Maribel by the boat) into play at Pilchard Bay, and a saved log continued, at 30 fps with saves; no red line at run time apart from 62 of the 63 geometry shaders (one-pass cube shadow maps), which do not build. Unit tests: the graphics tests that build pass (t_refl's two sample libraries are not on this machine); 13 that compile nvn_mtl.m with hle_nvn.c already failed to build on main (g_ft defined in both, hle_nvn.c symbols missing from their stubs)
- The user confirmed the build on the iPhone
- README: games table row and picture (the arrival on the island's dock); AGENTS.md: the list (forty games) and a note on the port; development.md: status
- Nintendo EPD's engine (code name Park; sead / agl / gsys, NintendoWare, Havok, Pia and NEX) on NVN; 9,766 BNSH shaders in zstd and Yaz0 SARC files and main, the in-flight movie VP9 + Vorbis through movie::Extractor, the Happy Home Paradise add-on mounted. A new game runs through Tom Nook's counter, the movie and the island's creation to the player on the dock at 30 fps; not yet seen: a save, which the game first makes at the end of day 1
- Yield back-off, for every game: a yield whose caller's x19–x28 moved since the last one is progress (the SDK's YieldThread reaches the SVC through tail branches, so they are the loop's own): it returns at once with no host call and the run starts again; polls keep those registers and back off as before. The game's SaveProc builds a new island in grid walks that yield once per cell: 77 s of black screen after the movie, now about 10 s. Breath of the Wild's PlacementMgr and RadarMgr yield per batch item too: outdoors on the Mac CPU energy 0.35–0.74 → 0.21–0.34 W, CPU 76–83% → 96–106% of a core, frame interval p99 19 → 20–21 ms
- Rewriter, for every game: halfword jump tables get landing pads up to their largest entry (half_count), not all 65,536: this main's patch area went from 42 MB, out of a branch's reach, to 7.9 MB; the other games' modules change only in such tables
- Host: main'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; Commit with a CommitOption, GetSaveDataRestoreFlag; the adjustable user clock, GetStandardSteadyClockCurrentTimePoint, oe::GetRegionCode, SDK 20's album share calls; video movie decoders keep 16 output slots (the game's player holds every one it is offered)
- Profile: render_cond on (the game skips clears with conditional rendering; without it every frame was black), pad_layout full, mem_used 122 MB, the prewarm list recorded from boot to the dock, the chain header and tables for the current chain version
The game copies its precomputed sky scattering tables (System/KSys/sky.skybin) into CPU-cached pools and flushes them; the host made the 256x32x16 3D table a GPU texture of zeros, so the sky came out brownish grey. nvnMemoryPoolFlushMappedRange now records the flushed ranges (nvn_flushed in hle_nvn.c), and tex_init_fill fills a new GPU texture from such storage once, as it did for CPU_NO_ACCESS pools. DM_NVN_FLUSH_FILL=0 turns it off; t_nvn covers the range bookkeeping.
- The user confirmed the build on the iPhone
- README: games table row and picture (Vincent climbing the first Nightmare stage); AGENTS.md: the list (thirty-eight games) and a note on the port; development.md: status
- Atlus's GFD engine with CRI middleware (CPK archives, ADX2 sound, Sofdec2 movies through movie::Decoder); 3,128 GLSLC shaders: 3,050 loose files in data/shaders_binary, 76 in main and 2 the game compiles at boot (captured on the Mac)
- Shader chain, for every game: this GLSLC (GPU code 1.15) writes each vertex shader as a VertexA part (shader type 0, the position) and a VertexB part (the other outputs) in one code area; shader_split.py reads type 0 as a vertex shader (sph_stage) and dmsh_build.py merges the two parts as Ryujinx does (vertex_ab). Without it every varying was lost and the screen was black. Every other profile's dmsh_chain.h rewritten with its c1_full / cb_extent lists carried over (the merge touches only type-0 vertex shaders, which no other game has); The Great Ace Attorney Chronicles checked on the Mac
- Host, for every game: nvnDeviceGetInteger reports at least the GPU code version of the game's own GLSLC (the game refused to compile its built-in shaders against SDK 7's 1.5–1.14); nn::settings::GetAvailableLanguageCodeCount; nv12-uv-stride in the movie decoder's output format (CRI's player reads it unchecked and crashed on the first picture); a segment commit at the first pass boundary after a buffer → texture copy, since the game reuses the copy's staging memory a frame later (the subtitle band came out as another texture's blocks)
- Memory: mem_used 96 MB (the rewritten modules take 92 MB, so SetHeapSize failed and the game ran on a 32 MB heap)
- Prewarm list recorded from boot through the intro into the first Nightmare stage
- The newest version in the folder is packed (-p / PACKAGE picks another). A registered version with every shader_versions version present builds from the profile as before
- Any other version (the base game alone, another update, other build IDs) builds from a profile derived from it in <id>_build/profile: the profile's keys, that version's build row, and shader tables made from its own files; a red line names the modules whose per-build fixes do not apply
- Host prewarm skips shader keys the shader map does not have (a list of another game version), counted in the "prewarm done" line
- Tested with Hollow Knight: Silksong: the base game alone (1.0.27776) packs with its own 2,244 shaders and plays the opening on the Mac; base + 1.0.30000 builds as before
- README: the update is optional; Silksong's row no longer names it
- The user confirmed the build on the iPhone
- README: games table row and picture (Hornet's needle slash in Moss Grotto); AGENTS.md: the list (thirty-seven games) and a note on the port; development.md: per-game notes
- Unity 6000.0.50f1 IL2CPP with FMOD; the bundles carry their type trees, so unityfs.py reads Unity 6's Shader objects with no layout written out (2,450 shaders, the one geometry shader does not build)
- Host: nn::hid::GetDebugPadStates, which Unity 6 polls every frame and the SDK's own aborts on (Result 0x2aca)
- Rewriter: the address-region node area cut on this sdk build (alias high-water mark +3.00 GB → +2.26 GB in the first room)
- profile_guess.py: every game with an NPDM system resource gets the 5 GB block; heapless ones (Silksong, Absolum, Pokémon Legends: Z-A) used to get a 256 MB alias region
- Prewarm list recorded from boot to the first two rooms of Moss Grotto
- The user confirmed the spin wait build on the iPhone (the first build ran Rogueport at 24 fps: its three job workers polled the job list with no SVC, 99c3f91)
- README: games table row and picture (Rogueport's docks); AGENTS.md: the list (thirty-six games) and a note on the port; development.md: status
A loop that polls memory with no SVC costs nothing on the Switch, where a higher-priority thread of its core preempts it; here it takes a host core. TTYD's three
gfx::ParallelManager workers (main+0xa8e680) spun at 94% of a core each in Rogueport on an iPhone. patch_nso.py now copies every loop that provably cannot leave by
itself (only loads, register and flag arithmetic, direct branches and barriers on every path back to its head, nothing live at the head written; an exclusive add undone
on every path allowed) into the patch area; the copy's back-edges pass a spin gate, where the host polls on for 2 ms and then sleeps with the yield back-off
(dm_spin_wait; DM_SPIN, DM_SPIN_POLL). Mac Rogueport route beside the committed build: frame p95 29-37 -> 18-22 ms, CPU 93-129% -> 38-57%. The x18 scan decodes in
blocks without details, so the rewrite is faster than before (TTYD main 20.5 -> 11.7 s, Tears of the Kingdom 101.5 -> 64.2 s); its sites are unchanged.
- The game does not step by the time that passed: its frame update (main+0x6a337c) takes a fixed 1/30 s, loaded at main+0x667904, so with only the frame limiter at 60 it ran twice as fast on the iPhone. A second cut loads the 1/60 that rodata already holds (main+0x167a834), the same two words as the community's 60 fps cheat for this build (patch_nso.py). The user confirmed normal speed on the iPhone
- The earlier note that the game steps by elapsed time came from a 0.8 s walk compared by eye on the Mac; the docs now say so, keep the frame-count conversions of that 1/30 as they are, and note that ladders misplay at 60 with the cheat too
- README: games table row and picture; AGENTS.md: the list (thirty-five games) and a note on the port; SDK 4.5.0 is now the oldest so far, not Cuphead's 5.5.0
- FromSoftware's engine (dantelion2) with Havok and FMOD on SDK 4.5.0, the oldest so far; subsdk0 is the movie module (H.264 MP4, decoded by the host). The logos and opening movie, the title (offline), the first-run settings, character creation with the name through the inline keyboard, the opening cutscene and the Undead Asylum cell, at 60 fps with music and sound; about 1.6 GB resident, flat. Not yet tested on an iPhone
- Shaders: 3,099, of which 3,040 are GLSLC outputs, one per .vpo / .fpo entry of six BND3 archives inside the dvdbnd archives (.bdt indexed by .bhd5, DCX zlib entries), read by shader_split.py (bhd5_entries, undcx, bnd3_members, bdt_shaders), and 59 more in main
- SDK 4's GLSLC writes control sections whose +0x20 is 12 bytes short of their length; outputs the old rule rejects are read through their section headers (glslc_section). On every output the old rule keeps (1,073 in five games) both give the same key; it also recovers 8 shaders of Fire Emblem: Three Houses and 3 of Age of Calamity (GLSLC 0x1d.0), whose tables and chains are regenerated (6661409043d9, 55b1235ad307; Three Houses' prologue battle checked on the Mac)
- SDK 4 calls: the movie decoder constructor without a priority and movie::Buffer without a sync object; friends::GetBlockedUserList without a Uid (empty, success); the UTF-16 profanity masking by name (the name entry crashed in the SDK's version)
- Poll / Select with nothing to wait on (no descriptors, or only negative ones) wait out their timeout and return 0, as on Horizon, in every mangling including SDK 4's Select: the game's network thread spun a core at 100% (dm_guest_sleep_ns)
- Graphics: handles read from uniform buffers take the base of the descriptor pool bound at replay (OP_SET_TEX_POOL / OP_SET_SAMP_POOL now tracked): with 12 sampler pools holding different samplers under one ID, the map walls took a clamp-to-edge sampler and smeared sideways. The bound pools' bases live in each command buffer (cmd_t) instead of a 64-entry list that overflowed
- 60 fps (30 on the Switch): a cut on the render thread's frame limiter constant (main+0x706fa4); the game steps by the time that passed, so the same input walks the same distance at 30 and 60
- The real-time declaration of the acquiring thread covers 1.1 x the window's peak as well as 1.25 x the p90 (rt_comp_tick): frames past the declaration stalled 30-60 ms, about once a second in this game at 30 and at 60 fps. This is the same rule for every game; CPU energy here is unchanged
- Profile: pad_layout full (the game confirms with B, positional); docs: 03-bringup, 04-graphics, development notes
The mouse-as-touchscreen monitor swallowed every left click in the game
window, so the close and fullscreen buttons never saw their clicks (and the
game took them as touches). The content view, DmGameView, now takes the
clicks itself; the title bar, the window buttons and the resize edges keep
theirs. Touches are normalized to the centered 16:9 picture the present fits
into the view and clamped, as on the iPhone (DmGamepad.m), so they no longer
drift in fullscreen or in a window of another shape.
AppKit runs ⌘ shortcuts only through menu items, so the window now has a
main menu: Quit (⌘Q) and Window > Enter Full Screen (⌃⌘F). Only key-down
events with ⌘ go to it; a key released while ⌘ is held still reaches the
game, or it would stay pressed.
Co-authored-by: nocmt <nocmtone@gmail.com>
stdout carries one JSON object per line: stage, step, progress (done, total,
unit, est, note, frac, eta, elapsed; null while unknown), stage_end and say,
which carries 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; the
ticker now runs in json mode too and sends a count that was held back, so a
stage whose count moves rarely (the rewrite) no longer shows a stale count
until it ends. json mode never prompts; close() stops the ticker by an event
of its own and keeps the mode, so a second build in one Spyder session gets
its redraws again.
dm/tools/tests/test_nsp_progress.py covers the events, the held-back count,
the failed step's tail, the CLI error and the bar / lines / off output,
with no game files, keys or Xcode.
Co-authored-by: nocmt <nocmtone@gmail.com>
- Intelligent Systems' engine on NintendoWare (G3d, Vfx, Atk) with AngelScript, SDK 15.3.2 (Celeste's sdk build), NVN; subsdk0 is the movie module (WebM, VP9 + Vorbis). The title, a new file, the storybook prologue and the opening into Rogueport, with Mario under control on the docks, with music and sound; no crash in eight minutes. Not yet tested on an iPhone
- Shaders: the game names its zstd files .zst (8,726 of 9,959), so the split found none; shader_split.py now decompresses zstd by its magic. 11,858 BNSH in shaders/*.bfsha and effect/particle/*.ptcl; no other profile's RomFS on hand changes (Super Mario Galaxy's GDDR3.zst is a Vulkan game's)
- nn::settings::GetAvailableLanguageCodes (the language lookup in nnMain aborted in sm::Initialize)
- Memory: malloc's StandardAllocator reserves 62 GB of address space with virtual address memory, cut to 4 GB on main; Absolum's node area and 32 MB alignment cuts on this sdk build (Celeste shares it and has no virtual address memory: its rewrite differs only in those three words and it still boots); Engage's 5 GB block
- Rewriter: sub sp, sp, xN rounds xN up to 16 first (site_sp_align). AngelScript's ARM64 call bridges reserve stack arguments by their summed size (4 for one int), which Horizon tolerates and Apple's kernel faults on; of the 22 games' modules on hand only this main has the form, so every other rewrite is byte-identical. insn_census.py counts the sites
- Graphics: tex_dyn_slots for 2,386 pixel shaders that pick a combined texture slot at run time; arrays of handles in a uniform buffer (the reflection shaders: element k = the handle at byte 8k), read by the host's reflection reader and bound by the replay with no chain change; array elements of another type get the zero cube silently; ClearColori / ClearColorui; a format-converting copy into a packaged texture upgrades it to render-target use; an unbound uniform slot reads the zero bank. t_refl case for the handle array
- 60 fps (30 on the Switch): the game steps its clocks by the 60 Hz vsyncs it measures between frames, at least 2. Cuts on 1.0.1's main lower the floor to 1 and give the base clock the host's frame step (FRAME_STEP stub at main+0x2e77f8); profile fps_target 60 with fps_hold off (holding late frames made them slower: the frame waits for three render workers, idle during a hold, and the hold stuck at 20 fps). Game time 1.000-1.003 of real time on the Mac; fades take as long as at 30 fps. DM_FPS_TARGET=30 keeps 30
- Profile: pad_layout full (the D-pad opens Gear and Journal), prewarm list recorded through the opening; docs: 03-bringup, 04-graphics, development notes, profile README
Unity 2018.3.10f1 IL2CPP with FMOD; its sdk and movie module are DRAGON QUEST XI S's builds, so
that build's rewriter cuts apply as they are, and no host change was needed. Brought up on the Mac
through the opening movie, the title, options, the tutorial and the prologue (choices, menu,
scenario chart, log) at 60 fps as on the Switch, with sound and saves; no red line at run time,
0.6 GB resident at the title and 0.77 GB after ten minutes of story.
- unityfs.py: Unity 2018.3's Shader layout (SHADER_2018_3, type hash 8db76007...). The game has no
bundles and its serialized files carry no type trees, so the shader split stopped. Written from
AssetRipper's TypeTreeDumps for 2018.3.10f1 and checked node for node against the dump (1,325
nodes with the alignment flags); all 48 Shader objects read to their exact end (450 subprograms,
452 shaders with main's GLSLC outputs). Other profiles' hashes are untouched.
- Memory: the hardware pool (3.2 GB) in Engage's 5 GB block. Unity 2018.3 reserves no address
regions of its own, so every mapped page lies in StandardAllocator's region (377 MB at the peak).
- Input: the game reads no touch, so it stays docked with the 2D pad; the left stick moves the menu
cursor.
Development notes, the graphics catalogue and the survey's list of Unity versions read.
A rerun after an interrupted IPA pack did a lot of the build again:
- The iOS shaders were keyed on the shader split's key, and that covers every dm/gfx/tools/*.py.
A pull that changed only the Unity readers (shader_split.py, unityfs.py) made Monster Hunter
Rise split again and then restore all 63,111 shaders from the cache (9 minutes). The iOS shaders
are now keyed on the shader package's content and on what dmsh_build.py loads (the repository
modules it imports, at any depth, and the chain lock). A split that runs again and writes the
same package leaves them reused; hashing Rise's 510 MB package takes 0.3 s.
- The DLC was extracted on every run (Rise: 252 DLC, 16-30 s). It is now reused while the DLC NSPs
(names, sizes, times) and nx_extract's sources are unchanged (aoc.key).
- Xcode's custom build location setting overrode -derivedDataPath, so every game's app was built
in one shared ~/Library/Developer/Xcode/DerivedData/Build: a build after another game's compiled
all 129 files, and two games built at once wrote the same app. build_ipa.sh sets SYMROOT and
OBJROOT under the game's work folder; a repeat build there compiled nothing (1 s).
Checked: Celeste with its split forced to run again (same package, iOS shaders reused), Rise's DLC
twice (15.9 s, then 0.1 s), and an Xcode build into a path with spaces and an apostrophe.
Each game's next build runs its iOS shader step once more, to write the new key.
The game asks for present interval 2 (Unity's 30 fps setting) and nothing else holds it at 30;
display mode presents a frame every tick and the iPhone's display link runs at 60 Hz. Its
animations follow the time that passed: from one save, screenshots at the same moments of the move
to the merchant match at 30 and 60 fps (mean difference 3.9, against 11.0 for frame counting).
DM_PRESENT_INTERVAL=game keeps 30.
At 60 fps a ReadFile into texture pool memory failed with EFAULT in two Mac runs of three: the
dirty-page tracker can write-protect the pages again between dm_guest_write_prepare and pread.
h_fs_ReadFile now prepares the rest of the buffer again and reads on (up to eight times); four runs
at 60 fps: one retry, no failed read.
Unity 2022.3.54f1 IL2CPP on Super Mario Galaxy's sdk build, with Ace Attorney Investigations
Collection's memory layout (1 GB pool, 5 GB block). Three general changes made it play:
- svcGetInfo type 24 (FreeThreadCount): the host answered 0, and Unity creates a thread only when
nn::os::GetFreeThreadCount is at least 1, so its main thread waited at boot for a thread it never
asked for. The host answers 96 (pm's application limit) less the game's live threads, as the
kernel does.
- Movie audio decoders keep 64 outputs (32 since DRAGON QUEST XI S): Unity's movie player starts
only once it holds about half a second of audio, measured at 27 outputs with a peak of 30. Trace
lines (DM_MOVIE_TRACE) name the decoder.
- shader_split.py reads Unity ComputeShader objects (unity_compute), with Unity 2022.3.54f1's layout
in unityfs.py: the post-processing stack's 85 kernels, the colour grading LUT baker among them, so
the battle scene is no longer black. Files whose ComputeShader has no known tree are reported and
skipped: Cuphead's and Engage's tables split byte-identical.
Also nvnSyncSetDebugLabel (a no-op). The game reads no touch, so it stays docked with the 3D pad
layout. README games table, AGENTS.md game list, development notes and the bring-up / graphics
catalogues.
The host writes 60 into Unreal's t.MaxFPS and 1 into nvn.SyncInterval (hle_fpslock.c, this build's rows); DM_FPS_TARGET=30 in Documents/env.txt keeps the game's 30. Mac (M4 Pro): the field holds 60 fps (frame time p95 17 ms, GPU 6.6 ms p50) at normal speed with sound. README, AGENTS.md and the development note say so
New profile for Square Enix's Unreal Engine 4.18 game: from the opening movie and the title
through the prologue into free play at Cobblestone Tor, 30 fps with sound and saves, no red
line. An IPA builds from the NSP folder; it has not run on an iPhone yet.
- Files and shaders: Unreal 4.18 legacy (version 4) paks with LZ5 blocks, decoded on the Mac
with the game's own LZ5_decompress_safe (uepak.py, hle_oodle.c serving after nninitStartup);
38,892 shaders inline in the cooked packages as zlib-compressed LSLG records (shader_split.py
ue_legacy_jobs, ue_inline_shaders; the NSP script and profile_guess know both pak kinds).
- Memory: Unreal's address region capped at 4064 MB (game_args, MB up to 4095); SDK 7.3.2 cuts
in patch_nso.py (node area 62 MB, 32 MB alignment for large regions; the build Odyssey shares,
which boots unchanged) for Engage's 5 GB block. Rewriter: a jump table whose bounds check sits
right before the load (check_before_copy).
- Host, for every game: audio catch-up frames wait for queue space (Square Enix's sound driver
never waits on the renderer event: 11,000 updates a second before); movie decoder events read
at signal time as the SDK does, audio decoders keep 32 outputs (the player plays only once
every track holds one: the opening movie never started in half the boots), and no input after
an end-of-stream packet until Flush / Stop (a skipped movie mapped 2.25 GB of end-of-stream
samples); ec shop accessors, friends, auto-sleep, album, file data cache and movie hooks.
- Graphics, for every game: a geometry shader that only picks the layer (Unreal's
FWriteToSliceGS, the colour-grading LUT) folded into the vertex shader's layer entry points
(dmsh_build.py gs_slice / vs_layer_entry, nvn_mtl.m prog_fold_gs); cube-map array depth counted
in layer-faces in the Metal texture, the upload and the guest layout (t_nvn checks it);
nvnTextureGetTextureAddress and the command memory size / free getters.
- Shader chain changed (dmsh_build.py): DQ11 rebuilt for Mac and iOS; every other profile's
dmsh_chain.h rewritten with its c1_full / cb_extent lists carried over (no c1 or uniform read
touched). Mac checks on this host: Big Brain Academy (movie loops, 23 catch-up renders as
before), Monster Hunter Rise (movies), Super Mario Odyssey (boot on the cut sdk).
- MonoGame for the Switch, its C# and the game's compiled to C++ by Sickhead's BRUTE into main; FMOD Studio through AudioOut; only main and sdk. No host change: the logos, the title, a new file and the prologue at 60 fps with music and sound, no red line, about 1.65 GB resident, flat over five minutes. Not yet tested on an iPhone
- The shaders are MonoGame effects (MGFX version 8): 13 in Content/Effects/*.xnb and SpriteBatch's built-in one in main. shader_split.py reads them (mgfx_effect), each effect parsed to its end; a shader's bytecode is u32 control length, the control section, u32 code length, the code area. 26 shaders (20 pixel, 6 vertex), every binding decodes. A pass may name no vertex shader and keep SpriteBatch's (10 of the 13 effects), so they pair as single-stage programs. Cuphead's and 13 Sentinels' tables split again unchanged
- NSP script: a provisional profile never got the sys_resource the survey lists, so the host answered 32 MB of system resource for every new game; write_profile writes it now (Celeste: 0, as its NPDM declares)
- Survey: XNB files and the MonoGame and FMOD markers (profile_guess.py), and a note on where a MonoGame game's shaders are
- Prewarm list recorded through boot, the title, a new file and the prologue
The IPA carries no entitlements (build_ipa.sh builds it unsigned), so the memory limit depends on how the player signs it. Without com.apple.developer.kernel.increased-memory-limit iOS gave the app 3,376 MB on an iPhone 18 Pro Max and closed Hades II within minutes (#30); with it the limit is 6,144 MB, which every iPhone log.txt here shows (footprint + os_proc_available_memory headroom). Peak footprints in those logs: Hyrule Warriors: Age of Calamity 5,359 MiB, Hades II 3,466-3,842 MiB (#30), Breath of the Wild 3,196 MiB, Cuphead 3,184 MiB.
Since 56b8061 the split reads the GLSLC outputs of every main and pairs only vertex shaders of single-stage sources (GLSLC, Unreal, MT Framework) with the cover, but these two profiles kept the tables of the old split, so the NSP script's comparison failed ("the split shader tables differ from game profile ...").
- Fire Emblem: Three Houses: shader_pairs.tsv 415 -> 398 rows. Its 17 BNSH vertex shaders (nn::ui2d's __ArchiveShader.bnsh in DATA1.bin and two BNSH archives in main) keep the pairs of their own variations and lose only the cover 042f574560d4d29c, so vs_pad gives them only the outputs their own pixel shaders read. nn::gfx binds a variation's stages as one program, and the 246 pipelines in the Mac pso_user.tsv agree: 6 of the 17 were drawn, each only with a pixel shader of its own variation.
- Fire Emblem Engage: the 11 GLSLC shaders Unity has in main are added (shader_map.tsv 9270 -> 9281 rows, one compute row in shader_facts.tsv, 6 cover pairs for its 3 vertex shaders). No existing row changed.
Chain procedure for both: dmsh_chain.h (Three Houses b-58aa432ce3c0, Engage b-1e9d905b402b), Mac and iOS libraries rebuilt (the same metal failures as before: Three Houses 2, Engage 1), c1_full.tsv and cb_extent.tsv regenerated (Three Houses: header lines only). Mac boots of 100 s: Three Houses pre-builds the same 234 pipelines as before, those with the 17 vertex shaders among them, with no pipeline failure and only its known cube texture red line; Engage has no red lines.
With DM_WINDOW=1 every connected pad with an extended gamepad profile goes to dm_pad_set, the path the iPhone's controllers take, ORed with the keyboard (a non-zero stick wins). The buttons are mapped by position as DmGamepad.m read_pad does: the right face button is A, the bottom B, the top X, the left Y; shoulders L R, triggers ZL ZR, Menu + and Options -, both sticks and their presses. Connecting and disconnecting are logged and rescan the pads; when the last one goes away its buttons and sticks are released.
Changed from the PR: the PR ORed the pad into the keyboard's dm_input_set in push() and mapped the labels straight across (an Xbox pad's bottom A was the Switch's A), so one pad pressed different buttons on the Mac and the iPhone. No wireless discovery: pads paired in the system's Bluetooth settings connect on their own.
Built for Super Mario Odyssey; not yet run with a controller here (the PR played Odyssey with an XInput pad on an M1 Pro).
Co-authored-by: JY626 <151988630+JY626@users.noreply.github.com>
AGENTS.md lists them among the playable games (thirty), with what each needed; the README shows
screenshots from Mac runs (Whack Match; Patrick's Parabox's Intro). Both start in the 2D touch layout;
in Big Brain Academy the touches no control takes go to the game's touch screen (handheld). The
profiles and the per-game notes say playable on an iPhone.
- movie::Buffer::Data() is Base() + Offset() and Capacity() the whole buffer (STEINS;GATE RE:BOOT's subsdk0), so a PCM or picture output whose read offset the client had advanced was written past its end, over the guest heap. GetOutputBuffer resets the range first. Native-texture mode is left as it is: there Data() is the texture object the client points it at. An end-of-stream packet as long as the whole input buffer carries no data, as one with time -1 already did.
- Creating the output AudioUnit takes about 24 KB of stack (measured on the Mac), more than some game threads have left, and OpenAudioOut runs on the game's thread: it now runs on a host thread, once per AudioOut. Not HLE_FORCE_BIG, which would hide the call site the open hooks log.
- CalculateSpanBetween and CalculateStandardUserSystemClockDifferenceByUser go through the time service session, which the host never opens: AbortImpl in the SDKs of STEINS;GATE RE:BOOT and Tears of the Kingdom, which imports both. The host computes what the service does: the span from the steady time points (the network times when the sources differ, else an error), and the user clock difference from the user clock offsets (0 when their sources differ or both have automatic correction on). CalculateSpanBetween is not in the PR.
- Snapshots carry the user clock context of GetSystemClockContext, which stays the same until the host clock is set; worked out per snapshot, it moved by a second at random, which the user difference would report as a clock change.
- t_time: 14 checks on host snapshots and edited copies; it fails without the two hooks.
Mac: Monster Hunter Rise headless for 75 s with real (muted) audio: OpenAudioOut from the game's thread, AAC and H.264 movies through GetOutputBuffer, 60 fps, no movie red lines. No profile with a matching shader cache on this drive calls the two time functions.
Co-authored-by: Raghav Agarwal <88780872+futurist-raghav@users.noreply.github.com>
- NintendoWare Bezel Engine 1.56: nearly all of RomFS is in Bezel Engine archives (.bea, magic SCNE, every entry a zstd frame). shader_split.py reads them (bea_members): 2,125 shaders, most of them the engine's own in System.nx.bea. The 12 geometry shaders of Bezel's line renderer do not build (the main menu chart's outlines are missing); ten pixel shaders that choose their texture handle between two of the game's uniform buffers at run time do not load. The survey names Bezel Engine archives (profile_guess.py, the NSP script's notes)
- nninitStartup picks virtual address memory (a 60 GB StandardAllocator region) or a heap by IsVirtualAddressMemoryEnabled, which the host answered yes for every game. The new profile key sys_resource reports the NPDM's system resource (0 here); the survey writes it for new games, profiles without it keep the 32 MB answer
- SDK 11 calls that reach a service: GetHardwareOpusDecoderWorkBufferSize(int, int), the IR camera (every camera unconnected), LoadCachedNintendoAccountInfo, Enable / DisableIndividualFileDataCache, ldn::GetNetworkConfig and the socket overloads with plain sockaddr
- Movie decoder: the activity previews loop with Flush and Start from four threads. An input index the game held across a flush went back to the free list and was handed out twice; the fifth pending packet overwrote the decoder's mutex and the process died in 2 runs of 5. A send now claims such an index, held indices are withheld for 250 ms after a flush, pend[] cannot overflow, a flush keeps each slot's last picture and an output with no picture leaves the client texture as it was (the black flash at every loop). DM_MOVIE_TRACE=1 logs every decoder call
- The crash handler takes SIGABRT; a command buffer with no GPU start time no longer counts the whole clock as GPU time
- Handheld with Boost (op_mode, perf_mode): handheld, the game reads the touch screen; Boost gives 1920x1080
- 60 fps, sound, previews and saves; the title, enrollment, the menus and activities of every category. About 1.5 GB resident at the first activity, 1.86 GB after 15 minutes of activities (the game's pools as each activity loads). Prewarm list recorded through enrollment and a full activity. Not yet tested on an iPhone
Each tick took the newest queued frame and dropped the older ones, so when two frames landed in the same refresh (a 60 fps game whose frames take 16-17.5 ms) one was never shown and the next stayed on screen 33 ms. A tick now takes the oldest queued frame, and with three or more waiting it skips to the one before the newest, so a frame is shown at most one tick late. Dropped frames are still counted. DM_PRESENT_FIFO=0 takes the newest, as before.
The PR measured Dark Souls Remastered on an M4 Pro: presents within 14.7-18.7 ms 98.17 % -> 99.98 %, game enqueue to display p50 +2 ms. Not yet measured here: a windowed Super Mario Odyssey A/B on the Mac never had focus (the game suspends itself out of focus and its frames were discarded), and nothing has run on an iPhone yet.
Co-authored-by: MeNass89 <267055942+MeNass89@users.noreply.github.com>
nn::pctl::CheckFreeCommunicationPermission(bool) had no takeover: the SDK's version calls the pctl service (pctl::detail::ipc::GetService), which the host never opens. It now answers false with or without UI, as TryBeginFreeCommunication and IsFreeCommunicationAvailable do. DRAGON QUEST XI S imports it; none of the profiles do yet.
t_pctl resolves the hooks by their mangled names in the real swforce registrations: 37 checks, and it fails when the new hook is missing.
t_condvar_stub.c defines g_key18, which hle_thread.c reads: make -C dm/host/tests run did not link on main. All 13 test programs pass.
Co-authored-by: BeeeeeMo <14192706+BeeeeeMo@users.noreply.github.com>
- Row wrap: Maxwell reads a texture buffer and a 1D texture with the same instructions (TLD with integer coordinates only, TLDS Texture1DLodZero) and the bound descriptor decides. Without a type answer the translator read such a slot as 2D at (x, 0) and the host bound a dummy for a buffer texture, so it read 0. The vendored translator (InstEmitTexture.cs, marked nxnative:) now addresses those fetches as (x & 0x3FFF, x >> 14) and reports the slot (fetch1d); the host binds a buffer texture there as a linear 2D texture of 16,384-texel rows over the same memory (tbuf_as_2d, nvn_tbuf_rows), cached by texture serial. KTGL's light and decal lists: Age of Calamity 16,438 shaders, Three Houses 321 (torches now light its map, blood decals are lit); FFT 5, Monster Hunter Rise 3
- Shadow vertex inputs: for each vertex shader with integer inputs the build writes <key>_bv (dmsh_vin.py), which declares every integer input again as float4 and as the other-sign integer at attribute indices 16-30 and ORs them into the input; the host sends each attribute to the index of its format's class and the zero buffer to the others (vin_ct, vin_on). Age of Calamity's Mac battle: 0 vertices converted on the CPU instead of 545,000 per submission, submit + present 10.0 -> 5.7 ms a frame, CPU 3.97 -> 2.44 W, the same picture. DM_NVN_VCONV_CPU=1 gives the CPU conversion back
- A texture type mismatch no view bridges is logged once per shader and slot with its texture ID, NVN target and formats (tex_mis_note); it showed Age of Calamity's remaining one: 3D slots 8 and 9 of three vertex shaders bound with 2D textures (bound as is, as before)
- Chain procedure for every profile: the 18 with game files on this drive rebuilt for Mac and iOS (c1_full.tsv / cb_extent.tsv: header lines only; new cb_extent.tsv for Hades II, Cuphead and Monster Hunter Rise), the others header-only with their lists carried over. Mac smoke runs of 13 games: no new red lines
- Tests: t_tbuf2d (row aliases against texture_buffer views), t_vin (shadow inputs against the CPU conversion, every format class), test_dmsh_vin.py, nvn_tbuf_rows in t_nvn
AGENTS.md lists it among the playable games (twenty-eight), with what it needed; the README
shows a screenshot from a Mac run. It starts in the 2D touch layout; its quick save (R3) is in the 3D one.
- M2's ADV engine (m2lib, E-mote characters, PSB archives); 87 H.264 / AAC movies through the SDK's movie library in subsdk0; 487 shaders in five BNSH in main; 60 fps, 1280x720 in both modes by the game's own choice
- nn::audio::SetAudioOutVolume / GetAudioOutVolume: a gain on the samples the host plays; the SDK's versions read the audout session the host never made (null read at the first movie)
- Storage-sharing texture table: a new texture that takes over the storage of an entry under another description evicts it, unless a live texture object still resolves to it or a lookup found it in the last 16 submissions. The game decodes every picture into one reused region of a pool that lives for good: 1.58 GB of GPU memory 24,000 frames into a new game before, about 1.0-1.15 GB over 15 minutes of story after. Captain Toad's opening course unchanged (no takeover). The memory line counts unreferenced entries and takeovers
- DM_PROBE_WATCH=<address>[+<length>]: who writes a guest word, with the page kept protected and every store to it carried out by the fault handler through a writable alias; found the game's render size setter
- Probe backtraces read each frame with vm_read_overwrite (code without frame pointers keeps data in x29)
- Prewarm list recorded from boot through the prologue into the story
AGENTS.md lists it among the playable games (twenty-seven), with what it needed; the game notes describe the port. It starts in the 2D layout; the screenshot (the first episode's trial) is from a Mac run.
- Capcom's MT Framework: ARC archives (version 7, zlib entries) under nativeNXx64. Its 460 shaders (230 vertex, 230 pixel) are in one shader package (SPK, sc\NX_64\root in sa/NX_64/root.arc), each a code area followed by its control section; shader_split.py reads it (mtarc_members, mtspk_shaders) and pairs the vertex shaders with the single-stage cover. All build, every binding decodes (Mario's combined layout)
- SDK 10 calls that reach a service, from the import list against the hooks: oe::DisableAlbumImageTakenNotification (the first boot's crash), oe::IsInStoreDemoModeEnabled, vi::ListDisplays, fs::RenameDirectory, account save thumbnails, the ec::ShowShop overloads without an ApplicationId, friends::Start*FriendRequest, and ngc::ProfanityFilter's UTF-8 masking, SetMaskMode and Finalize called by name. nvnDeviceSetIntermediateShaderCache and nvnCommandBufferDiscardColor / DiscardDepthStencil are no-ops
- Black screen: the engine copies its textures into a CPU-cached pool with the CPU after creating them and never flushes (main has no cache maintenance at all): tex_flush_upload and tex_dirty_upload, as FINAL FANTASY TACTICS
- 30 fps (the game's own pacing), sound, H.264 movies, saves, the DLC reported in Special Contents; about 1.1 GB resident, flat over 14 minutes from the title to the first trial
- Prewarm list recorded through the first-boot notices, the title, the opening movie and the first episode's opening scenes
- The survey names MT Framework ARC archives (profile_guess.py, the NSP script's notes)
AGENTS.md lists it among the playable games (twenty-six), with what it needed; the game notes describe the port. It starts in the 2D layout; the screenshot is from a Mac run.