diff --git a/AGENTS.md b/AGENTS.md index c5c4c9a..ab0e848 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Notes for agents -- This repository runs Nintendo Switch games natively on Apple platforms: the game's own code runs, rewritten only where Apple's kernel requires it (`dm/tools/patch_nso.py`), on a host that answers Horizon's SVCs and the SDK's services (`dm/host`) and implements NVN, Vulkan and NVIDIA's desktop OpenGL on Metal (`dm/gfx/nvn`, `dm/gfx/vk`, `dm/gfx/gl`). Sixty-two games are playable on the iPhone; the [games table](dm/games/README.md#games) lists each with its build, SDK, engine and graphics API, and links its note, `dm/games//README.md`: what its profile needs and why, what was found on it, how to drive it on the Mac, what is open. [README.md](README.md) is the player-facing page (games, packing, playing); [docs/development.md](docs/development.md) has the internals, the script's options, building by hand, prewarm lists and power. +- This repository runs Nintendo Switch games natively on Apple platforms: the game's own code runs, rewritten only where Apple's kernel requires it (`dm/tools/patch_nso.py`), on a host that answers Horizon's SVCs and the SDK's services (`dm/host`) and implements NVN, Vulkan and NVIDIA's desktop OpenGL on Metal (`dm/gfx/nvn`, `dm/gfx/vk`, `dm/gfx/gl`). Sixty-five games are playable, each tested on the Mac or the iPhone (a Mac gameplay test is enough to count); the [games table](dm/games/README.md#games) lists each with its build, SDK, engine and graphics API, and links its note, `dm/games//README.md`: what its profile needs and why, what was found on it, how to drive it on the Mac, what is open. [README.md](README.md) is the player-facing page (games, packing, playing); [docs/development.md](docs/development.md) has the internals, the script's options, building by hand, prewarm lists and power. - Where to start: | Task | Read first | diff --git a/README.md b/README.md index f6da8c4..373d2b0 100644 --- a/README.md +++ b/README.md @@ -120,19 +120,20 @@ Under each game: the IPA's size, and "60 fps with 1.2.0" where the game runs at Pokémon Legends: Z-A
Pokémon Legends: Z-A
~6 GB Pokémon Sword
Pokémon Sword
~12.4 GB -Raging Loop
Raging Loop
~0.9 GB +Portal
Portal
~2.6 GB +Raging Loop
Raging Loop
~0.9 GB Rhythm Heaven Groove
Rhythm Heaven Groove
~3 GB ROMANCE OF THE THREE KINGDOMS 8 REMAKE
ROMANCE OF THE THREE KINGDOMS 8 REMAKE
~16.8 GB · controller only -Stardew Valley
Stardew Valley
~0.6 GB +Stardew Valley
Stardew Valley
~0.6 GB STEINS;GATE RE:BOOT
STEINS;GATE RE:BOOT
~6 GB Super Smash Bros. Ultimate
Super Smash Bros. Ultimate
~17.5 GB · offline -Taiko Risshiden V DX
Taiko Risshiden V DX
~3.3 GB · Japanese · played by touch +Taiko Risshiden V DX
Taiko Risshiden V DX
~3.3 GB · Japanese · played by touch Unicorn Overlord
Unicorn Overlord
~4 GB diff --git a/dm/games/010015100B514000/README.md b/dm/games/010015100B514000/README.md index 2c14dfc..f2f86ae 100644 --- a/dm/games/010015100B514000/README.md +++ b/dm/games/010015100B514000/README.md @@ -6,30 +6,31 @@ Base game v0 (1.0.0, SDK 16.2.0), update 1.0.1 (v65536, SDK 16.2.0; short Mac cr | Setting | Where | Why | |---|---|---| -| Resource heap +16 KB | [`patches.py`](patches.py) `CUTS`, v0 `main` `cd6e42ae…` at `main+0x1e62fc`, 1.0.1 `main` `f91868b8…` at `main+0x1f6890`, 1.2.1 `main` `ff773e90…` at `main+0x1cf9b0` | a resource heap came up about 0x70 bytes short; see [Memory](#memory) | | 5 GB guest layout: `mem_reserve 0x140000000`, `mem_alias 0xB8000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | [`game.tsv`](game.tsv) | 256 MB in front + 2.875 GB alias region + 256 MB stack region + 1.625 GB heap region: the game maps 1.75 GB of virtual address memory on demand into the alias region and asks for a 1.47 GB heap | | `shader_versions v0 v327680` | `game.tsv` | the shader tables are the union of both versions, so packing either needs both NSPs | -No `dm/tools/sdk_patches.py` cut for its `sdk` builds (`3053b229…` v0, `e4bd58ca…` 1.2.1). No `render_cond`: a minute's boot counted 65,536 or more clears under render conditions, and the game needs none. No `pad_layout`: the virtual gamepad starts in the 2D layout. +No game-specific executable cuts. No `dm/tools/sdk_patches.py` cut for its `sdk` builds (`3053b229…` v0, `e4bd58ca…` 1.2.1). No `render_cond`: a minute's boot counted 65,536 or more clears under render conditions, and the game needs none. No `pad_layout`: the virtual gamepad starts in the 2D layout. ## Versions - v0 (1.0.0), 1.0.1 (v65536) and 1.2.1 (v327680). With 1.2.1 layered over v0, unchanged files become APFS clones, so the merged RomFS adds only about 114 MB to the 3.5 GB base. -- 1.2.1 is a new `main` build (SDK 16.2.12). Its resource-heap cut was carried over from v0 with `dm/tools/nso_fnmap.py` (containing function `0x1e603c` → `0x1cf6f0`, identical after address masking; `ExpHeap::create` `0x4aac38` → `0x4c4a18`). - 1.2.1 imports 28 SDK functions v0 did not, which the host answers: `nn::nfp` (amiibo, initialized at boot) finds none, `pl::IsRunningOnOunce` (is this a Switch 2) is false, `ec::ShowShopApplicationInformationForUpgrade` is a no-op, `account::PopOpenUsers` returns none and `aoc::CountAddOnContent` none. Mario only reads the count from `PopOpenUsers` and falls back to `TryOpenPreselectedUser` on 0, and counts add-on content only on a Switch 2 (`hle_sdk16.c`, `hle_account.c`, `hle_misc.c`). - 1.2.1 compresses the NACP's title area (a u16 size, then raw deflate inflating to 32 entries of 0x300 bytes), which LibHac 0.19 does not decode; `nx_extract` and the NSP script do. Its patch NCA (key generation 17) makes hactool stop with `Invalid BKTR layout!`, which is why `nx_extract` uses LibHac. ## Memory -- **Current: function matching on unregistered main builds.** `MATCH_CUTS` in `patches.py` recognizes the complete 1,420-byte resource-heap setup function and its direct callees/imports, then derives the cut at function +0x2c0. The v0 and 1.2.1 code require separate fingerprints: a directly called helper changed from 72 to 76 bytes. Both fingerprints locate exactly the corresponding verified cut. A full rewrite of 1.2.1 with a synthetic unregistered build ID produced a byte-identical `main.patched.bin` to its known-build rewrite. This verifies patch selection and rewriting; it is not a new gameplay or iPhone test. Known builds keep their fixed cuts; an unfamiliar or ambiguous implementation is reported and skipped. This covers compatible code in later builds, not arbitrary update compatibility. See [Function-matched cuts](../../../docs/reference/rewriter.md#function-matched-cuts-for-unregistered-builds) for the shared mechanism and its limits. +- **Resolved: resource heaps exhausted by oversized host command records.** The game measures scratch recordings while building model resources (`main+0x57500`, shape/pass recording `+0x5b8c0`, mesh draw measurement `+0x593ec`, 1.2.1). Those measurements feed the GPU chunk size, although the resource heap itself is fixed by Nintendo's resource-size table. The measured templates contain uniform and vertex-buffer bindings whose host records were larger than the native commands. Compacting these shared host records fixes the allocation. See [Model heaps sized by command records](../../../docs/reference/memory.md#model-heaps-sized-by-the-hosts-command-record-sizes). -- **Resolved for the tested 1.0.1 startup path: null memory pool after resource allocation.** The unpatched build (`main` `f91868b88f60d3d59009db3389fde314`, `sdk` `3053b22935e32ddf15ef81997603e5c2`) logged `BufferInitialize: pool has no storage (pool 0x0 ... size 0x280)` and crashed with `sig=11 addr=0x4 pc=h_nvnMemoryPoolGetFlags+0`. Its heap-size setup function is the unique full 1,420-byte address-masked match of v0's `0x1e603c`, at `0x1f65d0`; both calls of v0's `ExpHeap::create` (`0x4aac38`) map to `0x48b134`, whose full masked function also matches. The existing +16 KB cut therefore maps from `0x1e62fc` to `0x1f6890`, with the same asserted original word. On an Apple M5 Pro Mac, isolated saves and module copies reproduced the unpatched crash after 10.99 s (last reported frame 471); changing only this word ran for 35.74 s (last reported frame 1827), with no null-pool diagnostic or crash, before controlled termination. These are short headless correctness runs, not full gameplay, save, iPhone or performance acceptance. When only v0 and 1.0.1 are supplied, the script still derives this version's own shader tables because the checked-in union also needs 1.2.1. +- **Measured failure and fix (1.2.1).** With the old heap allowance disabled, `Model/PlayerMarioSuper.bfres` receives table value `0x80b660` + `0x1008`, a `0x80c668`-byte ExpHeap. Its final MatBlock chunk was `0xc000` bytes, leaving `0x648` free for a `0x6b8` descriptor: 112 bytes short. Payloads, block headers and alignment padding account for the entire heap, so this was not free-list corruption. Compact uniform bindings alone reduce the requested GPU budget from `0xb8d4` to `0xb76c` and still fail; compact single vertex bindings reduce it to `0xa6ec`, so the chunk becomes `0xb000` and the descriptor fits. Compact render-state records had no effect because these scratch templates do not contain them; that experiment was reverted. + +- **Validation and remaining scope.** On the Mac, fresh isolated saves and `DM_KEYS='5:A,7:A'` reproduce the unpatched 1.2.1 null-pool crash in about 8 seconds. The general fix passed that route, then 60-second headless runs of both 1.0.0 and 1.2.1 with additional A presses, each reaching reported frame 3300 without the heap allowance, null-pool diagnostic or crash. `t_nvn` passes 152 checks; `t_reorder_chain` passes 171 including compact/full uniform-binding pixel equivalence; `t_vbuf` passes 45 including compact vertex bindings and GPU readback. These game runs use no translated shader libraries and establish CPU allocation progress, not full rendering, gameplay or iPhone acceptance. A fresh 1.2.1 rewrite after removing the profile patch emits zero cuts and is byte-identical to the executable used in those successful runs. 1.0.1 was not rerun with this fix. + +- **Removed workaround.** The former +16 KB per-resource cuts at v0 `main+0x1e62fc`, 1.0.1 `+0x1f6890` and 1.2.1 `+0x1cf9b0`, and their `MATCH_CUTS` rules, are removed. The original v0 failure was the same null-pool symptom for `Model/NpcCommonerA.bfres` after its `0x9000` MatBlock chunk. The shared [function matcher](../../../docs/reference/rewriter.md#function-matched-cuts-for-unregistered-builds), originally verified using this workaround's 1,420-byte heap-setup function across v0 and 1.2.1, remains available to other profiles; Wonder no longer enrolls a rule. - The game reserves "virtual address memory" (1.75 GB in `System/HeapSize/*.HeapDef.byml`) and maps it into the alias region with `MapPhysicalMemory` on demand. With the alias region left after the node area too small, large resources were silently skipped and a null deref followed where they were used, also visible as `SetHeapSize` asking for much less than the pool. The 5 GB layout above fixed it. - Its two modules are about 74 MB, and its heap request matches hardware with the host's 64 MB of "used" memory. -- **Current: every resource heap gets +16 KB.** Each resource gets its own `sead::ExpHeap` sized resource-size-table value + 0x1008 (`main+0x1e603c` → `ExpHeap::create` `main+0x4aac38`, v0). The table values are Nintendo's measured peaks with almost no margin (Yoshi's animation file `PlayerYoshiAnm` uses 99% of its budget). `Model/NpcCommonerA.bfres` in the opening course (Course900) came up about 0x70 bytes short: its last GPU chunk (MatBlock, 0x9000 bytes, 4 KB aligned, `main+0x4290c0`) fit, but the chunk's 0x598-byte descriptor did not; a buffer was then built on a null memory pool and the game crashed on a null read at `main+0x106f5c8`. The cut makes `mov x0,x23` → `add x0,x23,#0x4000` (the `ExpHeap::create` size argument). - The single 128 MB NVN virtual memory pool (flag 0x200) is mapped onto one physical pool in one request, so the host aliases it (`hle_nvn.c`). Before, a failed `MapVirtual` gave a buffer with a null pool right after a scene change. -- How the heap was traced: probes on the gsys GPU allocator (`main+0x4290c0`) and the heap's `tryAlloc` (vtable slot +0x30); the `!xN` heap walk uses Mario's sead layout by default (`DM_PROBE_HEAP` `0,28,30,b0,c8`; [05, Probes](../../../docs/agents/05-debugging.md#probes-without-a-debugger-dm_probe)). +- How the heap was traced: probes on the gsys GPU allocator (`main+0x4290c0` in v0, `+0x4286e0` in 1.2.1) and the heap's `tryAlloc` (vtable slot +0x30); the `!xN` heap walk uses Mario's sead layout by default (`DM_PROBE_HEAP` `20,28,30,b0,c8`; [05, Probes](../../../docs/agents/05-debugging.md#probes-without-a-debugger-dm_probe)). ## Frame rate @@ -94,11 +95,11 @@ Game-specific findings; the general host changes they led to are listed under [F General host changes first needed here; each applies to every game: -- **Function matching for opt-in cuts across updates**: [Function-matched cuts](../../../docs/reference/rewriter.md#function-matched-cuts-for-unregistered-builds). Shared rewriter support; Wonder is the first enrolled profile. +- **Function matching for opt-in cuts across updates**: [Function-matched cuts](../../../docs/reference/rewriter.md#function-matched-cuts-for-unregistered-builds). Shared rewriter support first verified on Wonder; its heap workaround and enrollment have since been removed. - **NVN on Metal basics** in [rendering.md](../../../docs/reference/rendering.md): [8-bit index widening](../../../docs/reference/rendering.md#8-bit-index-buffers), [texture swizzles](../../../docs/reference/rendering.md#texture-swizzle-through-swizzled-metal-views) (`nvn_fmt_to_mtl`, `img_fit`), [format-changing render-target views](../../../docs/reference/rendering.md#render-target-views-that-change-the-format) (`attach`, `present_raw_src`), [storage-shared transient targets](../../../docs/reference/rendering.md#transient-render-targets-shared-by-storage), [inline uniform updates as in-order copies](../../../docs/reference/rendering.md#inline-uniform-updates-as-ordered-gpu-copies) snapshotted at encode (`mtl_sub_update`), [CPU-written linear textures](../../../docs/reference/nvn-objects.md#linear-textures-in-cpu-visible-pools) on the pool buffer (`tex_linear`), [integer vertex inputs](../../../docs/reference/rendering.md#integer-vertex-inputs) (`int_inputs`, `vfmt_for_ct`), [intra-encoder barriers](../../../docs/reference/rendering.md#metal-4-does-not-order-operations-inside-one-encoder) (`blit_buf`, `DM_NVN_BLIT_ORDER`), [zero-filled new textures](../../../docs/reference/rendering.md#new-textures-start-as-zero) (`tex_zero_fill`), [scissored zero clears](../../../docs/reference/rendering.md#scissored-colour-clears) (`clear_rect_zero`), [vertex output padding from shader pairs](../../../docs/reference/shaders.md#pixel-shader-inputs-the-vertex-shader-does-not-write) (`vs_pad`, `shader_pairs.tsv`), [virtual pools aliased onto one physical pool](../../../docs/reference/nvn-objects.md#virtual-memory-pools-mapped-onto-one-physical-pool). - **Binding layout and handles in uniform buffers** (`NVN_RB_TEXU` / `NVN_RB_SAMPU`): [04, section 3](../../../docs/agents/04-graphics.md#3-find-the-binding-layout-by-census); the compute code start: [Compute code with and without a header](../../../docs/reference/shaders.md#compute-code-with-and-without-a-header). - **The SDK's audio renderer in-process** and the catch-up frame between two updates: [Running the SDK's own renderer on the CPU](../../../docs/reference/audio.md#running-the-sdks-own-renderer-on-the-cpu), [At least one rendered frame between two updates](../../../docs/reference/audio.md#at-least-one-rendered-frame-between-two-updates). -- **Resource heaps cut by build ID** (`CUTS` in `patches.py`), **memory partition** (`mem_init`), **virtual NVN pools**: [Per-resource heaps sized from Nintendo's measured peaks](../../../docs/reference/memory.md#per-resource-heaps-sized-from-nintendos-measured-peaks), [Partitioning the guest block between alias region, stacks and heap](../../../docs/reference/memory.md#partitioning-the-guest-block-between-alias-region-stacks-and-heap), [Virtual memory pools mapped onto one physical pool](../../../docs/reference/nvn-objects.md#virtual-memory-pools-mapped-onto-one-physical-pool). +- **Compact buffer bindings keep model heaps within their budgets**, **memory partition** (`mem_init`), **virtual NVN pools**: [Model heaps sized by command records](../../../docs/reference/memory.md#model-heaps-sized-by-the-hosts-command-record-sizes), [Partitioning the guest block between alias region, stacks and heap](../../../docs/reference/memory.md#partitioning-the-guest-block-between-alias-region-stacks-and-heap), [Virtual memory pools mapped onto one physical pool](../../../docs/reference/nvn-objects.md#virtual-memory-pools-mapped-onto-one-physical-pool). - **Update imports diffed before the first run**, **fixed-address code gated by `module_t.build_id`**, **BFTTF shared font** (`hle_sdk16.c`), **the extension font**, **`GetCommandMemoryUsed` counting host segments**, **`NVNcommandBuffer` +0x10** (`nvn_flag10`), **`prog_t` size**: [A game update imports SDK calls the base game did not](../../../docs/reference/services.md#a-game-update-imports-sdk-calls-the-base-game-did-not), [Code inherited from another title at fixed addresses](../../../docs/reference/services.md#code-inherited-from-another-title-at-fixed-addresses), [Shared font address: BFTTF header or bare TTF](../../../docs/reference/services.md#shared-font-address-bfttf-header-or-bare-ttf), [Nintendo-extension glyphs for button icons](../../../docs/reference/services.md#nintendo-extension-glyphs-for-button-icons), [What `GetCommandMemoryUsed` counts](../../../docs/reference/nvn-objects.md#what-getcommandmemoryused-counts), [Inlined SDK code writes into `NVNcommandBuffer` +0x10](../../../docs/reference/nvn-objects.md#inlined-sdk-code-writes-into-nvncommandbuffer-0x10), [Host records must fit the game's SDK object sizes](../../../docs/reference/nvn-objects.md#host-records-must-fit-the-games-sdk-object-sizes). - **Exclusive windows** (fall-through exit first, Mario had 82): [Exit stub order in exclusive windows](../../../docs/reference/rewriter.md#exit-stub-order-in-exclusive-windows). **Ideal core −2**: [Ideal core −2 resolves to the process default core](../../../docs/reference/threads.md#ideal-core-2-resolves-to-the-process-default-core). - **Present-interval holds** (`g_link_hold_until`, `nvn_game_fps`): [Frames of a dynamic present interval held on screen and at the acquire](../../../docs/reference/frame-rate.md#frames-of-a-dynamic-present-interval-held-on-screen-and-at-the-acquire). @@ -107,7 +108,6 @@ General host changes first needed here; each applies to every game: ## Open - On the Mac, Mario's menu and dialogue text render blank (fine on the iPhone), per development.md's known gaps. -- Why the resource heap is short is not known (`patches.py`: every allocation size before that block is set by game code and data); Breath of the Wild's identical failure came from host record sizes, and whether Mario's chunk is sized the same way is not checked yet. - dm/games/README.md says the heap check turns "Wonder's 1.44 GB" request into a 0xc0000000 alias region; 03 says it asks for 1.47 GB, and the profile's alias region is 0xB8000000. - Several addresses in 02 and 03 (`main+0x1070910`, `main+0x107dbc0`, `main+0x4da5c`, `main+0xb8dec`, `main+0xcfd24`, `main+0x36ceff0`) do not name their build; the present-interval ones are from 1.2.1. - 2 agl post-effect compute shaders do not translate. diff --git a/dm/games/010015100B514000/game.tsv b/dm/games/010015100B514000/game.tsv index ba038d3..e133ced 100644 --- a/dm/games/010015100B514000/game.tsv +++ b/dm/games/010015100B514000/game.tsv @@ -1,4 +1,4 @@ -# Game profile of Super Mario Bros. Wonder (Switch, base game v0 and update 1.2.1). This folder also holds the shader tables (generated by shader_split.py), pso_manifest.tsv, patches.py (the rewriter's patches of its builds) and, not committed, c1_full.tsv and cb_extent.tsv +# Game profile of Super Mario Bros. Wonder (Switch, base game v0 and update 1.2.1). This folder also holds the shader tables (generated by shader_split.py), pso_manifest.tsv and, not committed, c1_full.tsv and cb_extent.tsv # Format: key value; several values of one key are separated by spaces; one build row per version; comments must be on their own line. Rebuild the host after changing memory / NVN constants; after changing shader-related keys follow the chain procedure in AGENTS.md # ---- Identity ---- diff --git a/dm/games/010015100B514000/patches.py b/dm/games/010015100B514000/patches.py deleted file mode 100644 index ee738bc..0000000 --- a/dm/games/010015100B514000/patches.py +++ /dev/null @@ -1,37 +0,0 @@ -# Super Mario Bros. Wonder (010015100B514000): the rewriter's patches of this game's own builds. dm/tools/patch_nso.py loads every profile's patches.py and applies those of a module's NSO build ID; -# CUTS, OBS and FRAME_STEP as described there ("Registered patches"). Patches of Nintendo SDK module builds are in dm/tools/sdk_patches.py -CUTS = { - # Super Mario Bros. Wonder v0 main - 'cd6e42aee7934f4d8393cd43893ae25e': [ - # Resource heap slack: each resource unit's ExpHeap is created with the resource size table value + 0x1008 (main+0x1e603c → ExpHeap::create main+0x4aac38); the table holds Nintendo's measured peaks with almost no margin - # (PlayerYoshiAnm uses 99%). On the host, Model/NpcCommonerA.bfres in the opening course Course900 is about 0x70 bytes short: after the model resource's last GPU chunk (MatBlock, - # 0x9000 bytes, 4 KB aligned, main+0x4290c0) is placed, its 0x598-byte descriptor does not fit, the GPU allocation fails → a buffer is built on a null memory pool → null read crash at main+0x106f5c8. - # Where this differs from hardware is not known (every allocation size before this block is determined by game code and data); for now every resource heap gets +16 KB. - (0x1e62fc, 'e00317aa', 'e0124091', 'resource heap size: mov x0,x23 → add x0,x23,#0x4000 (ExpHeap::create size argument +16 KB)'), - ], - # Super Mario Bros. Wonder 1.0.1 (v65536), main f91868b8...: resource heap slack as above. - # Ported from v0 by the unique address-masked match of the complete 1,420-byte function - # 0x1e603c -> 0x1f65d0; both ExpHeap::create call sites also map to the same callee - # (0x4aac38 -> 0x48b134). The original instruction is asserted by the rewriter. - 'f91868b88f60d3d59009db3389fde314': [ - (0x1f6890, 'e00317aa', 'e0124091', 'resource heap size: mov x0,x23 -> add x0,x23,#0x4000 (ExpHeap::create size argument +16 KB)'), - ], - # Super Mario Bros. Wonder 1.2.1 (v327680) main: same as above. Location carried over from v0 by dm/tools/nso_fnmap.py (containing function 0x1e603c → 0x1cf6f0 identical after address masking, ExpHeap::create 0x4aac38 → 0x4c4a18) - 'ff773e90972d544eb79406eaa65396d5': [ - (0x1cf9b0, 'e00317aa', 'e0124091', 'resource heap size: mov x0,x23 → add x0,x23,#0x4000 (ExpHeap::create size argument +16 KB)'), - ], -} - -# Unknown main builds: identify the complete heap setup function and its direct -# callees/imports, not an address or the short mov/add sequence. Verified against -# v0 (0x1e603c) and 1.2.1 (0x1cf6f0); a helper changed between them, so each has its -# own fingerprint. Known builds above keep their exact original-byte assertions. -MATCH_CUTS = {'main': [{ - 'name': 'resource heap slack', - 'size': 1420, - 'fingerprints': [ - '8062801c559542f56d41eda82685f85f0302933458a57fca074e9b3a3a475102', - '09233b021587d058cf645d301d58d07e3dc2bdbac1737b874bb7a0ba914725ed', - ], - 'cuts': [(0x2c0, 'e00317aa', 'e0124091', 'resource heap size: mov x0,x23 -> add x0,x23,#0x4000 (ExpHeap::create size argument +16 KB)')], -}]} diff --git a/dm/games/01007BB017812000/README.md b/dm/games/01007BB017812000/README.md new file mode 100644 index 0000000..2f4d33f --- /dev/null +++ b/dm/games/01007BB017812000/README.md @@ -0,0 +1,52 @@ +# Portal (`01007BB017812000`) + +Update 1.0.3 (`v196608`) over the base game, SDK 13.3.0. Valve's Source engine as NVIDIA Lightspeed Studios ported it to the Switch: `main` is a 0.5 MB launcher with SDL, and the engine is 28 NRO modules (`nro/*.nro` in the RomFS, registered by `.nrr/launcher_main.nrr`) loaded with `nn::ro`. It renders with NVN, its D3D9 layer linked into `materialsystem.nro`. Modules `main`, `subsdk0` (GLSLC), `subsdk2` (NintendoSDK_movie) and `sdk`; `subsdk1` is NVIDIA's OpenGL driver and is never loaded. + +**Current:** plays on the Mac at 60 fps: the opening movie, the title menu (its 3D background map), New Game, test chamber 00 with its portals (the view through them included), the pause menu and a saved game, with sound; no red line. About 1.5 GB footprint in the chamber. The unsigned IPA is built (2,823,721,518 bytes, about 2.6 GB); not yet tested on an iPhone. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `modules main subsdk0 subsdk2 sdk` | [`game.tsv`](game.tsv) | `main` imports from no subsdk; the NROs do: `video_nx.nro` plays the movies through the movie module and both it and `materialsystem.nro` call GLSLC ([Modules only the NROs import from](../../../docs/reference/services.md#modules-only-the-nros-import-from)) | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000`, `sys_resource 0x0` | `game.tsv` | no system resource, so no virtual address memory; the game sets a heap in the 4 GB layout. The rewritten modules fit the default 64 MB of used memory | +| `pad_layout full` | `game.tsv` | first-person: move with the left stick, look with the right | +| `shader_groups /game.zip/=game` | `game.tsv` | the shipped programs come from `game.zip`; the run-time captures go to `other` | + +## Rendering + +- **Resolved: the NSP script took it for an OpenGL game.** `main` imports 20 EGL functions and names `glShaderSource` (SDL's EGL backend and GL renderer), but draws nothing; `materialsystem.nro` and `video_nx.nro` import `nvnBootstrapLoader`. The first capture boot stopped with `[nvn-mtl] FATAL cannot read the shader map` from an NVN submit in `nro8` ([NVN in NROs under an SDL main that names GL](../../../docs/reference/rendering.md#nvn-in-nros-under-an-sdl-main-that-names-gl)). +- About 140 draws a frame at the title and in chamber 00; GPU p50 about 6 ms on an M4 Pro, NVN submit and present under 3 ms. + +## Shaders + +- The D3D9 layer links each Source shader combination (`hl2/shaders/fxc/*.vcs`, D3D9 bytecode) into one program and keeps the GLSLC output of every program in the game's precache list (`portal/glbaseshaders.cfg`, 219 entries) in `nxcontent/nvsb/.nvsb`: a 0x60-byte `NVSB` header (version 0x65, the file's length at +8), then one GLSLC output holding a vertex and a pixel shader. The game compiles the rest with `glslcCompile`: within 30 s of boot, 10 outputs (the movie's and the engine's own). +- **Resolved:** the split found none of the 219: they are deflated members of `game.zip` ([GLSLC outputs in a zip archive, and those NROs compile at run time](../../../docs/reference/shaders.md#glslc-outputs-in-a-zip-archive-and-those-nros-compile-at-run-time)). The map has 255 keys, 165 pixel and 90 vertex, 242 from the `.nvsb` files and 13 from the capture, with 229 vertex and pixel pairs. +- The game writes the programs it has linked to its save, `portal/glshaders/glshaders.cfg` (`-glshadersSaveFS` in its boot parameters): 219 entries after chamber 00, the precache list. More entries there mean programs compiled at run time on that route, which a fresh capture boot does not reach; on the iPhone each would give `[RED] shader … is not in the shader map`. + +## Files + +- The whole game is `game.zip` (2.7 GB, 41,416 members, Deflate or stored) beside the NROs; the Source file system reads it itself. The profile's `romfs_marker` is `game.zip`. +- The boot parameters are `nxcontent/rom_boot_params.txt` in the zip (`-game portal -glshadersSaveFS -nodev -nosteam +mat_queue_mode 2 +mat_picmip 0`); host arguments after `--` (a `+map` among them) change nothing. +- Only the merged update is registered. The base game's build has no profile row. + +## Movies + +- The opening movie (1280x720 H.264 with AAC) plays through the host's movie decoder from `video_nx.nro`, which shows the frames with NVN and plays the sound through its own `AudioOut`. + +## Mac routes + +- Run the command the NSP script prints for `--platform mac` (`DM_NVN_SHMAP` names this profile's `shader_map.tsv`), with `DM_CTL=` to drive it ([05-debugging.md](../../../docs/agents/05-debugging.md)). +- From a fresh save: the title menu at about 25 s; `A` (New Game), `A` (Chapter 1) loads test chamber 00 in about 15 s. The portal opens in the vault after GLaDOS's first lines (about 30 s); the right stick turns, the left stick walks through it. `Plus` opens the pause menu; `Down`, `A`, `A` saves (`portal/save/*.sav` in the save). + +## Found on this game + +- [Modules only the NROs import from](../../../docs/reference/services.md#modules-only-the-nros-import-from): the provisional profile adds the subsdks the NROs need. +- [NVN in NROs under an SDL main that names GL](../../../docs/reference/rendering.md#nvn-in-nros-under-an-sdl-main-that-names-gl): the graphics API follows the NROs' imports. +- [GLSLC outputs in a zip archive, and those NROs compile at run time](../../../docs/reference/shaders.md#glslc-outputs-in-a-zip-archive-and-those-nros-compile-at-run-time): the split reads zip members; the run-time capture also runs for NROs that call `glslcCompile`. +- [`ro::LookupModuleSymbol` for a name the module does not define](../../../docs/reference/services.md#rolookupmodulesymbol-for-a-name-the-module-does-not-define): the engine's `BuiltDebug` probe is answered as on Horizon, without a red line. + +## Open + +- iPhone test of the IPA. +- Later chambers (energy balls, turrets, water, the escape) are untested; watch `glshaders.cfg` and the log for programs outside the shipped 219. diff --git a/dm/games/01007BB017812000/bf_want.tsv b/dm/games/01007BB017812000/bf_want.tsv new file mode 100644 index 0000000..54caeb8 --- /dev/null +++ b/dm/games/01007BB017812000/bf_want.tsv @@ -0,0 +1 @@ +# generated diff --git a/dm/games/01007BB017812000/game.tsv b/dm/games/01007BB017812000/game.tsv new file mode 100644 index 0000000..1cb9136 --- /dev/null +++ b/dm/games/01007BB017812000/game.tsv @@ -0,0 +1,37 @@ +# Portal 1.0.3, SDK 13.3.0. Valve's Source engine (NVIDIA Lightspeed Studios' Switch port): 28 NRO modules, NVN, shaders as GLSLC outputs. +# Mac bring-up and validation: README.md beside this profile. + +# ---- Identity ---- +title_id 01007BB017812000 +program_id 01007BB017812000 + +# ---- App ---- +name Portal +ipa Portal.ipa +app_category public.app-category.games +pad_layout full + +# ---- Versions ---- +build v196608 1.0.3 main=8ec6b5335d7ca09e21c9b6989cfa23fe subsdk0=2f31216323724e1ff64f03eef5fe030d subsdk2=7e9f31f59e001cf5d9b546ffb233f480 sdk=2bf3519216f5b9d3e8ce26863c2e98b6 +package v196608 +shader_versions v196608 + +# ---- Files ---- +# main imports from none of the subsdks; the NROs import GLSLC (subsdk0: materialsystem.nro and video_nx.nro) and the movie library (subsdk2: video_nx.nro). +# subsdk1 is NVIDIA's OpenGL driver, never loaded +modules main subsdk0 subsdk2 sdk +romfs_marker game.zip +zip_store .nro:.zip:.metallib + +# ---- Memory ---- +# main sets a heap of at most the pool − 64 MB: a 3328 MB heap region +mem_reserve 0x100000000 +mem_alias 0x10000000 +mem_stack 0x10000000 +app_pool 0xCD500000 +# the NPDM declares 0 MB of system resource: IsVirtualAddressMemoryEnabled is false, as on Horizon +sys_resource 0x0 + +# ---- Shader split ---- +shader_groups /game.zip/=game +shader_note Portal shaders diff --git a/dm/games/01007BB017812000/pso_manifest.tsv b/dm/games/01007BB017812000/pso_manifest.tsv new file mode 100644 index 0000000..4620515 --- /dev/null +++ b/dm/games/01007BB017812000/pso_manifest.tsv @@ -0,0 +1,2 @@ +# Prewarm seed list (shipped in the app, next to shader_map.tsv). Rows: Sshader keystage / Rrender pipeline key (pso_skey hex) / Ccompute shader key. +# Provisional profile: empty until recorded on the Mac (DM_PSO_MANIFEST=off DM_PSO_RECORD). diff --git a/dm/games/01007BB017812000/shader_facts.tsv b/dm/games/01007BB017812000/shader_facts.tsv new file mode 100644 index 0000000..e665d36 --- /dev/null +++ b/dm/games/01007BB017812000/shader_facts.tsv @@ -0,0 +1 @@ +# Answer table for translation queries (generated by dm/gfx/tools/shader_split.py, do not edit): key fact JSON. This game has no PC version to pair with: only the thread group / shared memory of compute shaders (taken from the control section); other queries get the translator defaults diff --git a/dm/games/01007BB017812000/shader_map.tsv b/dm/games/01007BB017812000/shader_map.tsv new file mode 100644 index 0000000..157ca66 --- /dev/null +++ b/dm/games/01007BB017812000/shader_map.tsv @@ -0,0 +1,258 @@ +# kind: evidence Portal shaders (GLSLC in RomFS + main) → control section + code area keys (dm/gfx/tools/shader_split.py) +# column 5 records where each key first appears; this game has no PC version to pair with +package index stage dm_ctl_sha16 source +ps_game.bin 0 pixel 125514da6678b858 /game.zip/nxcontent/nvsb/1006143466.nvsb#GLSLC@0x60#var +vs_game.bin 0 vertex 22eb0bfcd9d1228a /game.zip/nxcontent/nvsb/1006143466.nvsb#GLSLC@0x60#var +vs_game.bin 1 vertex 64b47f72b72d3927 /game.zip/nxcontent/nvsb/1010343794.nvsb#GLSLC@0x60#var +ps_game.bin 1 pixel b22b24b3038b9d0c /game.zip/nxcontent/nvsb/1010343794.nvsb#GLSLC@0x60#var +vs_game.bin 2 vertex 17d9f2e860f8e2a5 /game.zip/nxcontent/nvsb/101163401.nvsb#GLSLC@0x60#var +ps_game.bin 2 pixel 7c7c7d32a06ab09c /game.zip/nxcontent/nvsb/101163401.nvsb#GLSLC@0x60#var +ps_game.bin 3 pixel 99aa47484f5af30c /game.zip/nxcontent/nvsb/1033516092.nvsb#GLSLC@0x60#var +vs_game.bin 3 vertex 9b19085e1ae9241e /game.zip/nxcontent/nvsb/1033516092.nvsb#GLSLC@0x60#var +vs_game.bin 4 vertex 0240ee3cfc1e35f2 /game.zip/nxcontent/nvsb/1039914026.nvsb#GLSLC@0x60#var +ps_game.bin 4 pixel fcf7fa623131c989 /game.zip/nxcontent/nvsb/1039914026.nvsb#GLSLC@0x60#var +ps_game.bin 5 pixel 921fb80b384ab537 /game.zip/nxcontent/nvsb/1044132038.nvsb#GLSLC@0x60#var +vs_game.bin 5 vertex f37dda4622020cfc /game.zip/nxcontent/nvsb/1044132038.nvsb#GLSLC@0x60#var +ps_game.bin 6 pixel 8bbb1dc10b37bc05 /game.zip/nxcontent/nvsb/1058274740.nvsb#GLSLC@0x60#var +vs_game.bin 6 vertex df3028266d9a6623 /game.zip/nxcontent/nvsb/1058274740.nvsb#GLSLC@0x60#var +ps_game.bin 7 pixel 120f0a976a6ca107 /game.zip/nxcontent/nvsb/1059071465.nvsb#GLSLC@0x60#var +vs_game.bin 7 vertex 4c4942c65f7c6532 /game.zip/nxcontent/nvsb/1059071465.nvsb#GLSLC@0x60#var +ps_game.bin 8 pixel 6a7f9ee0809b5334 /game.zip/nxcontent/nvsb/106531108.nvsb#GLSLC@0x60#var +vs_game.bin 8 vertex 6e54afd255e5d137 /game.zip/nxcontent/nvsb/106531108.nvsb#GLSLC@0x60#var +ps_game.bin 9 pixel c9276c56710f4233 /game.zip/nxcontent/nvsb/1070782498.nvsb#GLSLC@0x60#var +vs_game.bin 9 vertex f3a6513a949ba831 /game.zip/nxcontent/nvsb/1070782498.nvsb#GLSLC@0x60#var +ps_game.bin 10 pixel d3ccef3531f7f7ca /game.zip/nxcontent/nvsb/1072446736.nvsb#GLSLC@0x60#var +vs_game.bin 10 vertex dc9c688b97391b8e /game.zip/nxcontent/nvsb/1072446736.nvsb#GLSLC@0x60#var +vs_game.bin 11 vertex 08b91fd9a17e6f7c /game.zip/nxcontent/nvsb/109986375.nvsb#GLSLC@0x60#var +ps_game.bin 11 pixel 5aa93f9962c2b334 /game.zip/nxcontent/nvsb/109986375.nvsb#GLSLC@0x60#var +ps_game.bin 12 pixel 235dcef274b4241d /game.zip/nxcontent/nvsb/1104172461.nvsb#GLSLC@0x60#var +vs_game.bin 12 vertex c3a892980c9f1bc1 /game.zip/nxcontent/nvsb/1104172461.nvsb#GLSLC@0x60#var +ps_game.bin 13 pixel 78a6875febbcb08a /game.zip/nxcontent/nvsb/1112455901.nvsb#GLSLC@0x60#var +ps_game.bin 14 pixel fc840c3fd38ddeaf /game.zip/nxcontent/nvsb/1113526353.nvsb#GLSLC@0x60#var +vs_game.bin 13 vertex 15f7efa3c3d1cbfe /game.zip/nxcontent/nvsb/1121413061.nvsb#GLSLC@0x60#var +ps_game.bin 15 pixel 432321c03163b463 /game.zip/nxcontent/nvsb/1121413061.nvsb#GLSLC@0x60#var +ps_game.bin 16 pixel 0ea0e34795da2fc9 /game.zip/nxcontent/nvsb/1122469837.nvsb#GLSLC@0x60#var +vs_game.bin 14 vertex 1937761849fa62c5 /game.zip/nxcontent/nvsb/1127042807.nvsb#GLSLC@0x60#var +ps_game.bin 17 pixel f189ac57671cb167 /game.zip/nxcontent/nvsb/1127042807.nvsb#GLSLC@0x60#var +ps_game.bin 18 pixel e453c5167faa9012 /game.zip/nxcontent/nvsb/1134249875.nvsb#GLSLC@0x60#var +ps_game.bin 19 pixel 39baf2e91789d314 /game.zip/nxcontent/nvsb/1138337703.nvsb#GLSLC@0x60#var +vs_game.bin 15 vertex 64dc0a38de10b093 /game.zip/nxcontent/nvsb/1138337703.nvsb#GLSLC@0x60#var +vs_game.bin 16 vertex 6eb7c453d9d43f1a /game.zip/nxcontent/nvsb/1156613687.nvsb#GLSLC@0x60#var +ps_game.bin 20 pixel c0579baa232152ed /game.zip/nxcontent/nvsb/1156613687.nvsb#GLSLC@0x60#var +vs_game.bin 17 vertex 220019d2f2617b5b /game.zip/nxcontent/nvsb/1158978846.nvsb#GLSLC@0x60#var +ps_game.bin 21 pixel ce84ed175d35887d /game.zip/nxcontent/nvsb/1158978846.nvsb#GLSLC@0x60#var +ps_game.bin 22 pixel 5c5df1542b5bff8b /game.zip/nxcontent/nvsb/1176889401.nvsb#GLSLC@0x60#var +ps_game.bin 23 pixel 31e59687e92d4493 /game.zip/nxcontent/nvsb/1179368768.nvsb#GLSLC@0x60#var +vs_game.bin 18 vertex fd1e81f63ebb59b6 /game.zip/nxcontent/nvsb/1179368768.nvsb#GLSLC@0x60#var +ps_game.bin 24 pixel e69295f44bb2da6e /game.zip/nxcontent/nvsb/1186111763.nvsb#GLSLC@0x60#var +vs_game.bin 19 vertex f9f665e0884417d4 /game.zip/nxcontent/nvsb/1186111763.nvsb#GLSLC@0x60#var +vs_game.bin 20 vertex a14e721cbc57dbff /game.zip/nxcontent/nvsb/1212092448.nvsb#GLSLC@0x60#var +ps_game.bin 25 pixel eee64002261e2982 /game.zip/nxcontent/nvsb/1212092448.nvsb#GLSLC@0x60#var +ps_game.bin 26 pixel 4c505d7aa81ac07a /game.zip/nxcontent/nvsb/121709879.nvsb#GLSLC@0x60#var +ps_game.bin 27 pixel 6c8255959ab5796a /game.zip/nxcontent/nvsb/1218347587.nvsb#GLSLC@0x60#var +vs_game.bin 21 vertex 9b4ab611524d7406 /game.zip/nxcontent/nvsb/1218347587.nvsb#GLSLC@0x60#var +ps_game.bin 28 pixel 44b1330452231fa9 /game.zip/nxcontent/nvsb/1224245560.nvsb#GLSLC@0x60#var +vs_game.bin 22 vertex e19d5b5cd1e76f1d /game.zip/nxcontent/nvsb/1224245560.nvsb#GLSLC@0x60#var +vs_game.bin 23 vertex 6ae3eddfa6e6d35d /game.zip/nxcontent/nvsb/122467106.nvsb#GLSLC@0x60#var +ps_game.bin 29 pixel cb711d71f83d596d /game.zip/nxcontent/nvsb/122467106.nvsb#GLSLC@0x60#var +vs_game.bin 24 vertex 82d6dcb828e50229 /game.zip/nxcontent/nvsb/1227682125.nvsb#GLSLC@0x60#var +ps_game.bin 30 pixel ae84535932b4dcd1 /game.zip/nxcontent/nvsb/1227682125.nvsb#GLSLC@0x60#var +ps_game.bin 31 pixel 4385e34c64c07c1d /game.zip/nxcontent/nvsb/1232496238.nvsb#GLSLC@0x60#var +vs_game.bin 25 vertex 55e810a06cfe54c9 /game.zip/nxcontent/nvsb/1236251869.nvsb#GLSLC@0x60#var +ps_game.bin 32 pixel d6c6872f8d53e813 /game.zip/nxcontent/nvsb/1236251869.nvsb#GLSLC@0x60#var +ps_game.bin 33 pixel 991b15d95129104d /game.zip/nxcontent/nvsb/1246009064.nvsb#GLSLC@0x60#var +vs_game.bin 26 vertex 7931c599b8d30c1e /game.zip/nxcontent/nvsb/1260989403.nvsb#GLSLC@0x60#var +ps_game.bin 34 pixel 8b667d60c1621df4 /game.zip/nxcontent/nvsb/1260989403.nvsb#GLSLC@0x60#var +ps_game.bin 35 pixel 2cecc0c2d3bc8fb8 /game.zip/nxcontent/nvsb/1271581352.nvsb#GLSLC@0x60#var +vs_game.bin 27 vertex dd6fd1212d52a7bd /game.zip/nxcontent/nvsb/1271581352.nvsb#GLSLC@0x60#var +vs_game.bin 28 vertex 098fe645af1bc986 /game.zip/nxcontent/nvsb/1273760642.nvsb#GLSLC@0x60#var +ps_game.bin 36 pixel 34619f350df1d911 /game.zip/nxcontent/nvsb/1273760642.nvsb#GLSLC@0x60#var +ps_game.bin 37 pixel b41301328a3f8c28 /game.zip/nxcontent/nvsb/1281255338.nvsb#GLSLC@0x60#var +ps_game.bin 38 pixel ec0de43959c4c3d1 /game.zip/nxcontent/nvsb/1296462734.nvsb#GLSLC@0x60#var +ps_game.bin 39 pixel ce7143191485a7b0 /game.zip/nxcontent/nvsb/1296726989.nvsb#GLSLC@0x60#var +ps_game.bin 40 pixel b75b2d5db59429d1 /game.zip/nxcontent/nvsb/1300570570.nvsb#GLSLC@0x60#var +vs_game.bin 29 vertex 2c2ea14f8cdeaf49 /game.zip/nxcontent/nvsb/130100692.nvsb#GLSLC@0x60#var +ps_game.bin 41 pixel e73df20777fde6fd /game.zip/nxcontent/nvsb/130100692.nvsb#GLSLC@0x60#var +ps_game.bin 42 pixel 2d7cc82b0233a2ba /game.zip/nxcontent/nvsb/130708013.nvsb#GLSLC@0x60#var +vs_game.bin 30 vertex ce78e77ecb956ccb /game.zip/nxcontent/nvsb/130708013.nvsb#GLSLC@0x60#var +ps_game.bin 43 pixel a627e6f4c4137eec /game.zip/nxcontent/nvsb/1307602202.nvsb#GLSLC@0x60#var +ps_game.bin 44 pixel ec2ba5e99e4b2b73 /game.zip/nxcontent/nvsb/1322805238.nvsb#GLSLC@0x60#var +ps_game.bin 45 pixel 1a7ec2fdca342c58 /game.zip/nxcontent/nvsb/1323899066.nvsb#GLSLC@0x60#var +vs_game.bin 31 vertex ed8b93820ce36531 /game.zip/nxcontent/nvsb/1323899066.nvsb#GLSLC@0x60#var +ps_game.bin 46 pixel d0d674eb2c33f483 /game.zip/nxcontent/nvsb/134129515.nvsb#GLSLC@0x60#var +vs_game.bin 32 vertex 2b8af06ab22a32ec /game.zip/nxcontent/nvsb/1342895557.nvsb#GLSLC@0x60#var +ps_game.bin 47 pixel a939f620ce3cae1b /game.zip/nxcontent/nvsb/1342895557.nvsb#GLSLC@0x60#var +ps_game.bin 48 pixel b64f68523fc22633 /game.zip/nxcontent/nvsb/1375227838.nvsb#GLSLC@0x60#var +vs_game.bin 33 vertex e897f9e7b7e7abe1 /game.zip/nxcontent/nvsb/1375227838.nvsb#GLSLC@0x60#var +ps_game.bin 49 pixel 19c793ec1c918157 /game.zip/nxcontent/nvsb/1384086589.nvsb#GLSLC@0x60#var +ps_game.bin 50 pixel 03a090d0c141931f /game.zip/nxcontent/nvsb/1405799114.nvsb#GLSLC@0x60#var +ps_game.bin 51 pixel 0f156839a8257e94 /game.zip/nxcontent/nvsb/1408040754.nvsb#GLSLC@0x60#var +vs_game.bin 34 vertex 0fe802630f6ae611 /game.zip/nxcontent/nvsb/1409487133.nvsb#GLSLC@0x60#var +ps_game.bin 52 pixel cde80507748f5a63 /game.zip/nxcontent/nvsb/1409487133.nvsb#GLSLC@0x60#var +vs_game.bin 35 vertex 5b49a1a917bcf780 /game.zip/nxcontent/nvsb/141818572.nvsb#GLSLC@0x60#var +ps_game.bin 53 pixel d99b483a105b1cc1 /game.zip/nxcontent/nvsb/141818572.nvsb#GLSLC@0x60#var +vs_game.bin 36 vertex 71aaf555a1e3b18f /game.zip/nxcontent/nvsb/1424981946.nvsb#GLSLC@0x60#var +ps_game.bin 54 pixel f2a62bcd66cce52f /game.zip/nxcontent/nvsb/1424981946.nvsb#GLSLC@0x60#var +ps_game.bin 55 pixel 3f088c3551875e14 /game.zip/nxcontent/nvsb/1426380811.nvsb#GLSLC@0x60#var +ps_game.bin 56 pixel 2d116c497e9ce78a /game.zip/nxcontent/nvsb/1433953797.nvsb#GLSLC@0x60#var +vs_game.bin 37 vertex 47f7e99e85c0ddda /game.zip/nxcontent/nvsb/1433953797.nvsb#GLSLC@0x60#var +ps_game.bin 57 pixel 35e3e36d6f8bb1b5 /game.zip/nxcontent/nvsb/146048892.nvsb#GLSLC@0x60#var +ps_game.bin 58 pixel 6f34c9f005f22799 /game.zip/nxcontent/nvsb/1470967011.nvsb#GLSLC@0x60#var +vs_game.bin 38 vertex 17fdd14a505256a7 /game.zip/nxcontent/nvsb/1471683114.nvsb#GLSLC@0x60#var +vs_game.bin 39 vertex f9dc2debb451353d /game.zip/nxcontent/nvsb/1493701505.nvsb#GLSLC@0x60#var +ps_game.bin 59 pixel 0804a53aa42aa796 /game.zip/nxcontent/nvsb/1498732659.nvsb#GLSLC@0x60#var +vs_game.bin 40 vertex 603676c9302b8660 /game.zip/nxcontent/nvsb/1498732659.nvsb#GLSLC@0x60#var +ps_game.bin 60 pixel 827d5514d57b13b2 /game.zip/nxcontent/nvsb/1503210799.nvsb#GLSLC@0x60#var +vs_game.bin 41 vertex 5903d6c17975e5b7 /game.zip/nxcontent/nvsb/1516114854.nvsb#GLSLC@0x60#var +ps_game.bin 61 pixel bf73234069e015cc /game.zip/nxcontent/nvsb/1524623539.nvsb#GLSLC@0x60#var +ps_game.bin 62 pixel adbc6564e34e5b0e /game.zip/nxcontent/nvsb/1552135254.nvsb#GLSLC@0x60#var +ps_game.bin 63 pixel acca61b3b9f9e261 /game.zip/nxcontent/nvsb/1559279768.nvsb#GLSLC@0x60#var +ps_game.bin 64 pixel 140a361ef37048e1 /game.zip/nxcontent/nvsb/1559618750.nvsb#GLSLC@0x60#var +vs_game.bin 42 vertex ef8a777c2f151751 /game.zip/nxcontent/nvsb/1559618750.nvsb#GLSLC@0x60#var +ps_game.bin 65 pixel d2d785c5eac5767f /game.zip/nxcontent/nvsb/1563425968.nvsb#GLSLC@0x60#var +vs_game.bin 43 vertex d4135c17b4835b51 /game.zip/nxcontent/nvsb/1563425968.nvsb#GLSLC@0x60#var +ps_game.bin 66 pixel 127c8193afe5da34 /game.zip/nxcontent/nvsb/1570033706.nvsb#GLSLC@0x60#var +ps_game.bin 67 pixel 4d97dcb634d624b0 /game.zip/nxcontent/nvsb/1590887012.nvsb#GLSLC@0x60#var +ps_game.bin 68 pixel 3cbe87361ecb3a6a /game.zip/nxcontent/nvsb/1592819690.nvsb#GLSLC@0x60#var +vs_game.bin 44 vertex b7e6be8139f55e7c /game.zip/nxcontent/nvsb/1592819690.nvsb#GLSLC@0x60#var +ps_game.bin 69 pixel ab17b21738878ae3 /game.zip/nxcontent/nvsb/161257982.nvsb#GLSLC@0x60#var +vs_game.bin 45 vertex 5561f4ee52b8be94 /game.zip/nxcontent/nvsb/1613295314.nvsb#GLSLC@0x60#var +ps_game.bin 70 pixel c6ae9c4fe005b98d /game.zip/nxcontent/nvsb/1613295314.nvsb#GLSLC@0x60#var +ps_game.bin 71 pixel 3103745341950b5e /game.zip/nxcontent/nvsb/1620148918.nvsb#GLSLC@0x60#var +vs_game.bin 46 vertex 8eeae34269830c21 /game.zip/nxcontent/nvsb/1631216478.nvsb#GLSLC@0x60#var +ps_game.bin 72 pixel 5069030326b3cec7 /game.zip/nxcontent/nvsb/1655032705.nvsb#GLSLC@0x60#var +vs_game.bin 47 vertex 77b5ea971d43d4e7 /game.zip/nxcontent/nvsb/1659476259.nvsb#GLSLC@0x60#var +vs_game.bin 48 vertex 9cf62937439a145a /game.zip/nxcontent/nvsb/167503881.nvsb#GLSLC@0x60#var +ps_game.bin 73 pixel 460bdaf26dc5d5de /game.zip/nxcontent/nvsb/1717637429.nvsb#GLSLC@0x60#var +vs_game.bin 49 vertex ac10fac9a065513b /game.zip/nxcontent/nvsb/1717637429.nvsb#GLSLC@0x60#var +ps_game.bin 74 pixel 6e05837c9919d4d5 /game.zip/nxcontent/nvsb/1745741.nvsb#GLSLC@0x60#var +vs_game.bin 50 vertex 8e6823fe228f42c7 /game.zip/nxcontent/nvsb/1745741.nvsb#GLSLC@0x60#var +ps_game.bin 75 pixel 7f33688bc96eb5e6 /game.zip/nxcontent/nvsb/1755612563.nvsb#GLSLC@0x60#var +ps_game.bin 76 pixel 98ba0f0dffbe06fb /game.zip/nxcontent/nvsb/1764038594.nvsb#GLSLC@0x60#var +ps_game.bin 77 pixel 02210f9e7fe6ad4c /game.zip/nxcontent/nvsb/1780532206.nvsb#GLSLC@0x60#var +vs_game.bin 51 vertex f67c93a064c9e2ff /game.zip/nxcontent/nvsb/17832264.nvsb#GLSLC@0x60#var +ps_game.bin 78 pixel 1ce3d86b64df835d /game.zip/nxcontent/nvsb/1789278367.nvsb#GLSLC@0x60#var +ps_game.bin 79 pixel 257ad8b3a826f85c /game.zip/nxcontent/nvsb/1798622250.nvsb#GLSLC@0x60#var +vs_game.bin 52 vertex 864e005418823018 /game.zip/nxcontent/nvsb/1798622250.nvsb#GLSLC@0x60#var +ps_game.bin 80 pixel a830eb7f17c96b0c /game.zip/nxcontent/nvsb/1805388630.nvsb#GLSLC@0x60#var +ps_game.bin 81 pixel 4aa15ca517fe177c /game.zip/nxcontent/nvsb/1816372284.nvsb#GLSLC@0x60#var +vs_game.bin 53 vertex 7876a08ed4433271 /game.zip/nxcontent/nvsb/1816372284.nvsb#GLSLC@0x60#var +ps_game.bin 82 pixel 739f1d303eeede4d /game.zip/nxcontent/nvsb/1821191205.nvsb#GLSLC@0x60#var +vs_game.bin 54 vertex aa2461d24e7a8851 /game.zip/nxcontent/nvsb/1821191205.nvsb#GLSLC@0x60#var +ps_game.bin 83 pixel 1c1a25b7bd1ffb0c /game.zip/nxcontent/nvsb/1836539311.nvsb#GLSLC@0x60#var +ps_game.bin 84 pixel 13adbf4ebad1e85f /game.zip/nxcontent/nvsb/1853280091.nvsb#GLSLC@0x60#var +vs_game.bin 55 vertex 530f785ceae5e1f2 /game.zip/nxcontent/nvsb/1863332167.nvsb#GLSLC@0x60#var +ps_game.bin 85 pixel 5f7ee404f03da9d5 /game.zip/nxcontent/nvsb/1863332167.nvsb#GLSLC@0x60#var +ps_game.bin 86 pixel 018aec9caf171747 /game.zip/nxcontent/nvsb/1869408490.nvsb#GLSLC@0x60#var +vs_game.bin 56 vertex 561a5a407a91c63e /game.zip/nxcontent/nvsb/1870088145.nvsb#GLSLC@0x60#var +ps_game.bin 87 pixel f89c7245b208efb1 /game.zip/nxcontent/nvsb/1870088145.nvsb#GLSLC@0x60#var +ps_game.bin 88 pixel 34637fa90566a012 /game.zip/nxcontent/nvsb/1874471462.nvsb#GLSLC@0x60#var +ps_game.bin 89 pixel 0755826d2bc85e5e /game.zip/nxcontent/nvsb/18791119.nvsb#GLSLC@0x60#var +vs_game.bin 57 vertex 67342b5be7f8d037 /game.zip/nxcontent/nvsb/1895959455.nvsb#GLSLC@0x60#var +ps_game.bin 90 pixel eb9388e0c6cc0978 /game.zip/nxcontent/nvsb/1895959455.nvsb#GLSLC@0x60#var +vs_game.bin 58 vertex 1e89027c28140037 /game.zip/nxcontent/nvsb/1900815595.nvsb#GLSLC@0x60#var +ps_game.bin 91 pixel ad8d97ad16639353 /game.zip/nxcontent/nvsb/1907849095.nvsb#GLSLC@0x60#var +ps_game.bin 92 pixel fac044187b750b53 /game.zip/nxcontent/nvsb/1928481773.nvsb#GLSLC@0x60#var +vs_game.bin 59 vertex 2addf1736ec8b694 /game.zip/nxcontent/nvsb/1954356698.nvsb#GLSLC@0x60#var +ps_game.bin 93 pixel 75e8486686c3fa41 /game.zip/nxcontent/nvsb/1954356698.nvsb#GLSLC@0x60#var +ps_game.bin 94 pixel 2d30b32a6bc599ee /game.zip/nxcontent/nvsb/1958778744.nvsb#GLSLC@0x60#var +ps_game.bin 95 pixel 9c89de94351e8ce4 /game.zip/nxcontent/nvsb/1972493923.nvsb#GLSLC@0x60#var +ps_game.bin 96 pixel a9ab86ce5a9e0084 /game.zip/nxcontent/nvsb/1982246115.nvsb#GLSLC@0x60#var +vs_game.bin 60 vertex aaa9fae84306cf73 /game.zip/nxcontent/nvsb/198379649.nvsb#GLSLC@0x60#var +ps_game.bin 97 pixel f35ed9b3e12d8df0 /game.zip/nxcontent/nvsb/198379649.nvsb#GLSLC@0x60#var +ps_game.bin 98 pixel 5655df85fdb4995c /game.zip/nxcontent/nvsb/2028069179.nvsb#GLSLC@0x60#var +ps_game.bin 99 pixel 543d0b5664802983 /game.zip/nxcontent/nvsb/2073046031.nvsb#GLSLC@0x60#var +ps_game.bin 100 pixel 611385f7b64b0554 /game.zip/nxcontent/nvsb/2093901329.nvsb#GLSLC@0x60#var +ps_game.bin 101 pixel cdf3e03c37046bcd /game.zip/nxcontent/nvsb/2107668394.nvsb#GLSLC@0x60#var +ps_game.bin 102 pixel ed068a953918fd2e /game.zip/nxcontent/nvsb/2119418873.nvsb#GLSLC@0x60#var +ps_game.bin 103 pixel 0d590a627839a1a1 /game.zip/nxcontent/nvsb/2120931521.nvsb#GLSLC@0x60#var +vs_game.bin 61 vertex 852fe378741e13e0 /game.zip/nxcontent/nvsb/2120931521.nvsb#GLSLC@0x60#var +ps_game.bin 104 pixel d7be276590968fb3 /game.zip/nxcontent/nvsb/2141879921.nvsb#GLSLC@0x60#var +vs_game.bin 62 vertex 41b384743b379ffc /game.zip/nxcontent/nvsb/220851893.nvsb#GLSLC@0x60#var +ps_game.bin 105 pixel 4ae7d858a1ce0bb6 /game.zip/nxcontent/nvsb/220851893.nvsb#GLSLC@0x60#var +vs_game.bin 63 vertex 9966c3d73fa41fc6 /game.zip/nxcontent/nvsb/247584683.nvsb#GLSLC@0x60#var +ps_game.bin 106 pixel fcb9bff3a1754048 /game.zip/nxcontent/nvsb/267742669.nvsb#GLSLC@0x60#var +ps_game.bin 107 pixel 89d9b8e71cc50e62 /game.zip/nxcontent/nvsb/278474355.nvsb#GLSLC@0x60#var +vs_game.bin 64 vertex cf41f4f0e9d550a8 /game.zip/nxcontent/nvsb/278474355.nvsb#GLSLC@0x60#var +ps_game.bin 108 pixel 50982dbd53d67424 /game.zip/nxcontent/nvsb/304289249.nvsb#GLSLC@0x60#var +vs_game.bin 65 vertex eee444ab1c8d1058 /game.zip/nxcontent/nvsb/304289249.nvsb#GLSLC@0x60#var +ps_game.bin 109 pixel fe2f8ee3742e4e13 /game.zip/nxcontent/nvsb/304482634.nvsb#GLSLC@0x60#var +vs_game.bin 66 vertex 378b66633d4f2ff9 /game.zip/nxcontent/nvsb/375914922.nvsb#GLSLC@0x60#var +ps_game.bin 110 pixel c76a368ffddc4c71 /game.zip/nxcontent/nvsb/375914922.nvsb#GLSLC@0x60#var +ps_game.bin 111 pixel 44df06c3f986986d /game.zip/nxcontent/nvsb/387789906.nvsb#GLSLC@0x60#var +vs_game.bin 67 vertex 71960d32d8e77601 /game.zip/nxcontent/nvsb/387789906.nvsb#GLSLC@0x60#var +ps_game.bin 112 pixel a40400c45a261601 /game.zip/nxcontent/nvsb/40007417.nvsb#GLSLC@0x60#var +ps_game.bin 113 pixel dcf9179af9d94365 /game.zip/nxcontent/nvsb/407424802.nvsb#GLSLC@0x60#var +vs_game.bin 68 vertex e50deab2af6277c6 /game.zip/nxcontent/nvsb/407424802.nvsb#GLSLC@0x60#var +ps_game.bin 114 pixel 5812b4a5572bfd4f /game.zip/nxcontent/nvsb/409312457.nvsb#GLSLC@0x60#var +ps_game.bin 115 pixel 199e9deea0791910 /game.zip/nxcontent/nvsb/434480402.nvsb#GLSLC@0x60#var +vs_game.bin 69 vertex c625ee5057b83a6e /game.zip/nxcontent/nvsb/434480402.nvsb#GLSLC@0x60#var +ps_game.bin 116 pixel e1d4e9caa1a2c27c /game.zip/nxcontent/nvsb/442310971.nvsb#GLSLC@0x60#var +ps_game.bin 117 pixel 73e697229d75fff5 /game.zip/nxcontent/nvsb/461195124.nvsb#GLSLC@0x60#var +vs_game.bin 70 vertex 8d1a6ed5ed0cd8b3 /game.zip/nxcontent/nvsb/461195124.nvsb#GLSLC@0x60#var +vs_game.bin 71 vertex 93b71d2999931f8b /game.zip/nxcontent/nvsb/482272727.nvsb#GLSLC@0x60#var +ps_game.bin 118 pixel ffda0f4a59db00c7 /game.zip/nxcontent/nvsb/498388205.nvsb#GLSLC@0x60#var +ps_game.bin 119 pixel 844e66ce8a093f31 /game.zip/nxcontent/nvsb/500652001.nvsb#GLSLC@0x60#var +ps_game.bin 120 pixel 557810e3c0241394 /game.zip/nxcontent/nvsb/503501776.nvsb#GLSLC@0x60#var +vs_game.bin 72 vertex 68d378bfe9669b84 /game.zip/nxcontent/nvsb/504892865.nvsb#GLSLC@0x60#var +ps_game.bin 121 pixel bae43c936113b364 /game.zip/nxcontent/nvsb/504892865.nvsb#GLSLC@0x60#var +ps_game.bin 122 pixel e1b7adb71904c320 /game.zip/nxcontent/nvsb/521814475.nvsb#GLSLC@0x60#var +vs_game.bin 73 vertex 145fca62b42aa364 /game.zip/nxcontent/nvsb/527443616.nvsb#GLSLC@0x60#var +ps_game.bin 123 pixel c1a64bac795c3fe8 /game.zip/nxcontent/nvsb/527443616.nvsb#GLSLC@0x60#var +vs_game.bin 74 vertex 74dbf128fe4eb7b9 /game.zip/nxcontent/nvsb/531681361.nvsb#GLSLC@0x60#var +ps_game.bin 124 pixel ad62d00c8d19b6e5 /game.zip/nxcontent/nvsb/531681361.nvsb#GLSLC@0x60#var +ps_game.bin 125 pixel 912ea84d3cde9fa1 /game.zip/nxcontent/nvsb/567545403.nvsb#GLSLC@0x60#var +ps_game.bin 126 pixel 4071785723d70eda /game.zip/nxcontent/nvsb/569494234.nvsb#GLSLC@0x60#var +ps_game.bin 127 pixel 826d84bf0106f618 /game.zip/nxcontent/nvsb/600527269.nvsb#GLSLC@0x60#var +vs_game.bin 75 vertex a4efef32dc784f2a /game.zip/nxcontent/nvsb/606447035.nvsb#GLSLC@0x60#var +ps_game.bin 128 pixel e505b6c6b31c35b4 /game.zip/nxcontent/nvsb/606447035.nvsb#GLSLC@0x60#var +ps_game.bin 129 pixel 3466d2c7fb252da9 /game.zip/nxcontent/nvsb/638428005.nvsb#GLSLC@0x60#var +vs_game.bin 76 vertex f64141b26f258051 /game.zip/nxcontent/nvsb/640315710.nvsb#GLSLC@0x60#var +ps_game.bin 130 pixel 800e7c4f63e66de3 /game.zip/nxcontent/nvsb/671384613.nvsb#GLSLC@0x60#var +ps_game.bin 131 pixel 1c51322649f4afb8 /game.zip/nxcontent/nvsb/677047702.nvsb#GLSLC@0x60#var +ps_game.bin 132 pixel 8b47f1e49f4fe97f /game.zip/nxcontent/nvsb/679512904.nvsb#GLSLC@0x60#var +ps_game.bin 133 pixel d3c7ed8ce47cb1f1 /game.zip/nxcontent/nvsb/689002777.nvsb#GLSLC@0x60#var +vs_game.bin 77 vertex eafff9c848e0ed33 /game.zip/nxcontent/nvsb/69649018.nvsb#GLSLC@0x60#var +ps_game.bin 134 pixel ed626950e6cc09dd /game.zip/nxcontent/nvsb/69649018.nvsb#GLSLC@0x60#var +vs_game.bin 78 vertex 692cc50abe7105df /game.zip/nxcontent/nvsb/701819308.nvsb#GLSLC@0x60#var +vs_game.bin 79 vertex ad0dba73e3521c4f /game.zip/nxcontent/nvsb/705551276.nvsb#GLSLC@0x60#var +vs_game.bin 80 vertex 0834ba1b19dddaa6 /game.zip/nxcontent/nvsb/709681957.nvsb#GLSLC@0x60#var +ps_game.bin 135 pixel c7d8fb5ccb420fec /game.zip/nxcontent/nvsb/709681957.nvsb#GLSLC@0x60#var +ps_game.bin 136 pixel 04357952b0c0d87b /game.zip/nxcontent/nvsb/714097666.nvsb#GLSLC@0x60#var +ps_game.bin 137 pixel 9d2987c1051bd452 /game.zip/nxcontent/nvsb/715711902.nvsb#GLSLC@0x60#var +ps_game.bin 138 pixel ee1322856b3e2eef /game.zip/nxcontent/nvsb/722804612.nvsb#GLSLC@0x60#var +ps_game.bin 139 pixel 20599900215db911 /game.zip/nxcontent/nvsb/724234349.nvsb#GLSLC@0x60#var +vs_game.bin 81 vertex d12cdec8456761f0 /game.zip/nxcontent/nvsb/724234349.nvsb#GLSLC@0x60#var +vs_game.bin 82 vertex 7a895bba6277cb3a /game.zip/nxcontent/nvsb/724284755.nvsb#GLSLC@0x60#var +ps_game.bin 140 pixel 4bc885832a18a327 /game.zip/nxcontent/nvsb/729449742.nvsb#GLSLC@0x60#var +vs_game.bin 83 vertex 4e5c2c26c2d5bb34 /game.zip/nxcontent/nvsb/729449742.nvsb#GLSLC@0x60#var +ps_game.bin 141 pixel dc8db55426582cd7 /game.zip/nxcontent/nvsb/737508209.nvsb#GLSLC@0x60#var +ps_game.bin 142 pixel 2ab95b13cd096921 /game.zip/nxcontent/nvsb/738207507.nvsb#GLSLC@0x60#var +ps_game.bin 143 pixel e9cf6ea5e928b306 /game.zip/nxcontent/nvsb/845921421.nvsb#GLSLC@0x60#var +ps_game.bin 144 pixel 1d8653b8824a528d /game.zip/nxcontent/nvsb/846435117.nvsb#GLSLC@0x60#var +vs_game.bin 84 vertex 8641dc25284850be /game.zip/nxcontent/nvsb/846435117.nvsb#GLSLC@0x60#var +ps_game.bin 145 pixel 5ce263b79d071865 /game.zip/nxcontent/nvsb/847560579.nvsb#GLSLC@0x60#var +ps_game.bin 146 pixel cb8e93a74d272f3a /game.zip/nxcontent/nvsb/853867118.nvsb#GLSLC@0x60#var +ps_game.bin 147 pixel cd624981849cdad4 /game.zip/nxcontent/nvsb/880357657.nvsb#GLSLC@0x60#var +ps_game.bin 148 pixel 15f50a859b2116d4 /game.zip/nxcontent/nvsb/931054901.nvsb#GLSLC@0x60#var +ps_game.bin 149 pixel a5b11ef684da02ad /game.zip/nxcontent/nvsb/931143399.nvsb#GLSLC@0x60#var +vs_game.bin 85 vertex 3bcda7edf3254840 /game.zip/nxcontent/nvsb/939889222.nvsb#GLSLC@0x60#var +ps_game.bin 150 pixel efa738504e37e486 /game.zip/nxcontent/nvsb/939889222.nvsb#GLSLC@0x60#var +ps_game.bin 151 pixel 4d037aa1c724e9f0 /game.zip/nxcontent/nvsb/947159063.nvsb#GLSLC@0x60#var +vs_game.bin 86 vertex 92059018c9c06875 /game.zip/nxcontent/nvsb/947159063.nvsb#GLSLC@0x60#var +ps_game.bin 152 pixel 70589daf1c863703 /game.zip/nxcontent/nvsb/98083864.nvsb#GLSLC@0x60#var +ps_game.bin 153 pixel 048a4a4a753e5076 /game.zip/nxcontent/nvsb/988541672.nvsb#GLSLC@0x60#var +ps_game.bin 154 pixel 05d01429bfeb63df /game.zip/nxcontent/nvsb/989117612.nvsb#GLSLC@0x60#var +ps_other.bin 0 pixel d4c2351bc7c67576 /glslc_capture/1aff81dac9e05541.glslc#GLSLC@0x0#var +ps_other.bin 1 pixel 0cc5c6c1aa721048 /glslc_capture/2f09b055791ff1b1.glslc#GLSLC@0x0#var +vs_other.bin 0 vertex 6cb0028a6b7401c6 /glslc_capture/2f09b055791ff1b1.glslc#GLSLC@0x0#var +ps_other.bin 2 pixel b4de8e96999bc6a5 /glslc_capture/547ef86375c3a210.glslc#GLSLC@0x0#var +ps_other.bin 3 pixel 5295c34e7179442e /glslc_capture/66f9af325711b20a.glslc#GLSLC@0x0#var +vs_other.bin 1 vertex d3ff93e32f2f20dc /glslc_capture/66f9af325711b20a.glslc#GLSLC@0x0#var +ps_other.bin 4 pixel f22bdcee695d2c44 /glslc_capture/702acc26648e0805.glslc#GLSLC@0x0#var +ps_other.bin 5 pixel 9b50f1715fa0c3e8 /glslc_capture/73ab6333e91ffc82.glslc#GLSLC@0x0#var +ps_other.bin 6 pixel 1ffbb41ab2c5afb1 /glslc_capture/bd3c8633f57640bb.glslc#GLSLC@0x0#var +ps_other.bin 7 pixel ae25b90688edc871 /glslc_capture/d6bcf71d9f5be8dc.glslc#GLSLC@0x0#var +ps_other.bin 8 pixel 2a46639cf42dbba7 /glslc_capture/da9eb384b238fd55.glslc#GLSLC@0x0#var +vs_other.bin 2 vertex 79d859b20b8b60d7 /glslc_capture/efbae697500ffa02.glslc#GLSLC@0x0#var +ps_other.bin 9 pixel ab7f1002ad1561b1 /glslc_capture/efbae697500ffa02.glslc#GLSLC@0x0#var diff --git a/dm/games/01007BB017812000/shader_map.tsv.conflicts.tsv b/dm/games/01007BB017812000/shader_map.tsv.conflicts.tsv new file mode 100644 index 0000000..35c1b4d --- /dev/null +++ b/dm/games/01007BB017812000/shader_map.tsv.conflicts.tsv @@ -0,0 +1,2 @@ +# kind: evidence control-section key conflict rulings: empty +dm_ctl_sha16 chosen_pc class candidates records diff --git a/dm/games/01007BB017812000/shader_pairs.tsv b/dm/games/01007BB017812000/shader_pairs.tsv new file mode 100644 index 0000000..389b9b5 --- /dev/null +++ b/dm/games/01007BB017812000/shader_pairs.tsv @@ -0,0 +1,230 @@ +# kind: evidence vertex / pixel shader pairs within BNSH variations (generated by dm/gfx/tools/shader_split.py, do not edit): vertex key pixel key +0240ee3cfc1e35f2 fcf7fa623131c989 +0834ba1b19dddaa6 c7d8fb5ccb420fec +08b91fd9a17e6f7c 5aa93f9962c2b334 +08b91fd9a17e6f7c d3c7ed8ce47cb1f1 +098fe645af1bc986 34619f350df1d911 +098fe645af1bc986 99aa47484f5af30c +0fe802630f6ae611 cde80507748f5a63 +145fca62b42aa364 c1a64bac795c3fe8 +15f7efa3c3d1cbfe 19c793ec1c918157 +15f7efa3c3d1cbfe 31e59687e92d4493 +15f7efa3c3d1cbfe 432321c03163b463 +17d9f2e860f8e2a5 0755826d2bc85e5e +17d9f2e860f8e2a5 1c51322649f4afb8 +17d9f2e860f8e2a5 5ce263b79d071865 +17d9f2e860f8e2a5 7c7c7d32a06ab09c +17d9f2e860f8e2a5 adbc6564e34e5b0e +17fdd14a505256a7 99aa47484f5af30c +1937761849fa62c5 03a090d0c141931f +1937761849fa62c5 0f156839a8257e94 +1937761849fa62c5 127c8193afe5da34 +1937761849fa62c5 1ce3d86b64df835d +1937761849fa62c5 3f088c3551875e14 +1937761849fa62c5 4385e34c64c07c1d +1937761849fa62c5 4c505d7aa81ac07a +1937761849fa62c5 4d97dcb634d624b0 +1937761849fa62c5 5069030326b3cec7 +1937761849fa62c5 6f34c9f005f22799 +1937761849fa62c5 827d5514d57b13b2 +1937761849fa62c5 912ea84d3cde9fa1 +1937761849fa62c5 a40400c45a261601 +1937761849fa62c5 acca61b3b9f9e261 +1937761849fa62c5 ad8d97ad16639353 +1937761849fa62c5 b41301328a3f8c28 +1937761849fa62c5 ce7143191485a7b0 +1937761849fa62c5 e453c5167faa9012 +1937761849fa62c5 ec2ba5e99e4b2b73 +1937761849fa62c5 f189ac57671cb167 +1937761849fa62c5 fc840c3fd38ddeaf +1937761849fa62c5 fcb9bff3a1754048 +1937761849fa62c5 ffda0f4a59db00c7 +1e89027c28140037 99aa47484f5af30c +220019d2f2617b5b cd624981849cdad4 +220019d2f2617b5b ce84ed175d35887d +22eb0bfcd9d1228a 018aec9caf171747 +22eb0bfcd9d1228a 03a090d0c141931f +22eb0bfcd9d1228a 05d01429bfeb63df +22eb0bfcd9d1228a 0f156839a8257e94 +22eb0bfcd9d1228a 125514da6678b858 +22eb0bfcd9d1228a 127c8193afe5da34 +22eb0bfcd9d1228a 1ce3d86b64df835d +22eb0bfcd9d1228a 3f088c3551875e14 +22eb0bfcd9d1228a 4385e34c64c07c1d +22eb0bfcd9d1228a 4c505d7aa81ac07a +22eb0bfcd9d1228a 4d97dcb634d624b0 +22eb0bfcd9d1228a 5069030326b3cec7 +22eb0bfcd9d1228a 543d0b5664802983 +22eb0bfcd9d1228a 6f34c9f005f22799 +22eb0bfcd9d1228a 78a6875febbcb08a +22eb0bfcd9d1228a 827d5514d57b13b2 +22eb0bfcd9d1228a 912ea84d3cde9fa1 +22eb0bfcd9d1228a 991b15d95129104d +22eb0bfcd9d1228a a40400c45a261601 +22eb0bfcd9d1228a a627e6f4c4137eec +22eb0bfcd9d1228a acca61b3b9f9e261 +22eb0bfcd9d1228a ad8d97ad16639353 +22eb0bfcd9d1228a b41301328a3f8c28 +22eb0bfcd9d1228a ce7143191485a7b0 +22eb0bfcd9d1228a dc8db55426582cd7 +22eb0bfcd9d1228a e1d4e9caa1a2c27c +22eb0bfcd9d1228a e453c5167faa9012 +22eb0bfcd9d1228a ec2ba5e99e4b2b73 +22eb0bfcd9d1228a f189ac57671cb167 +22eb0bfcd9d1228a fc840c3fd38ddeaf +22eb0bfcd9d1228a fcb9bff3a1754048 +22eb0bfcd9d1228a ffda0f4a59db00c7 +2addf1736ec8b694 75e8486686c3fa41 +2b8af06ab22a32ec 34637fa90566a012 +2b8af06ab22a32ec 7f33688bc96eb5e6 +2b8af06ab22a32ec a939f620ce3cae1b +2c2ea14f8cdeaf49 e73df20777fde6fd +378b66633d4f2ff9 c76a368ffddc4c71 +3bcda7edf3254840 efa738504e37e486 +41b384743b379ffc 4ae7d858a1ce0bb6 +47f7e99e85c0ddda 2d116c497e9ce78a +4c4942c65f7c6532 120f0a976a6ca107 +4e5c2c26c2d5bb34 4bc885832a18a327 +530f785ceae5e1f2 5f7ee404f03da9d5 +530f785ceae5e1f2 9c89de94351e8ce4 +5561f4ee52b8be94 c6ae9c4fe005b98d +55e810a06cfe54c9 d6c6872f8d53e813 +561a5a407a91c63e 15f50a859b2116d4 +561a5a407a91c63e f89c7245b208efb1 +5903d6c17975e5b7 19c793ec1c918157 +5903d6c17975e5b7 31e59687e92d4493 +5903d6c17975e5b7 432321c03163b463 +5b49a1a917bcf780 d99b483a105b1cc1 +603676c9302b8660 0804a53aa42aa796 +64b47f72b72d3927 0ea0e34795da2fc9 +64b47f72b72d3927 b22b24b3038b9d0c +64b47f72b72d3927 b75b2d5db59429d1 +64dc0a38de10b093 39baf2e91789d314 +67342b5be7f8d037 a5b11ef684da02ad +67342b5be7f8d037 eb9388e0c6cc0978 +68d378bfe9669b84 bae43c936113b364 +692cc50abe7105df c0579baa232152ed +6ae3eddfa6e6d35d 844e66ce8a093f31 +6ae3eddfa6e6d35d c0579baa232152ed +6ae3eddfa6e6d35d cb711d71f83d596d +6cb0028a6b7401c6 0cc5c6c1aa721048 +6cb0028a6b7401c6 d4c2351bc7c67576 +6e54afd255e5d137 5c5df1542b5bff8b +6e54afd255e5d137 6a7f9ee0809b5334 +6e54afd255e5d137 8bbb1dc10b37bc05 +6e54afd255e5d137 d0d674eb2c33f483 +6e54afd255e5d137 ed068a953918fd2e +6eb7c453d9d43f1a 99aa47484f5af30c +6eb7c453d9d43f1a c0579baa232152ed +71960d32d8e77601 44df06c3f986986d +71aaf555a1e3b18f ee1322856b3e2eef +71aaf555a1e3b18f f2a62bcd66cce52f +74dbf128fe4eb7b9 ad62d00c8d19b6e5 +77b5ea971d43d4e7 34619f350df1d911 +77b5ea971d43d4e7 99aa47484f5af30c +7876a08ed4433271 2d7cc82b0233a2ba +7876a08ed4433271 4aa15ca517fe177c +7876a08ed4433271 cb8e93a74d272f3a +7931c599b8d30c1e 8b667d60c1621df4 +79d859b20b8b60d7 1ffbb41ab2c5afb1 +79d859b20b8b60d7 2a46639cf42dbba7 +79d859b20b8b60d7 9b50f1715fa0c3e8 +79d859b20b8b60d7 ab7f1002ad1561b1 +79d859b20b8b60d7 ae25b90688edc871 +79d859b20b8b60d7 b4de8e96999bc6a5 +79d859b20b8b60d7 f22bdcee695d2c44 +7a895bba6277cb3a e73df20777fde6fd +82d6dcb828e50229 ae84535932b4dcd1 +852fe378741e13e0 0d590a627839a1a1 +8641dc25284850be 1d8653b8824a528d +864e005418823018 257ad8b3a826f85c +8d1a6ed5ed0cd8b3 73e697229d75fff5 +8e6823fe228f42c7 6e05837c9919d4d5 +8eeae34269830c21 19c793ec1c918157 +8eeae34269830c21 31e59687e92d4493 +8eeae34269830c21 432321c03163b463 +92059018c9c06875 4d037aa1c724e9f0 +93b71d2999931f8b 34619f350df1d911 +93b71d2999931f8b 99aa47484f5af30c +9966c3d73fa41fc6 0804a53aa42aa796 +9b19085e1ae9241e 34619f350df1d911 +9b19085e1ae9241e 99aa47484f5af30c +9b4ab611524d7406 6c8255959ab5796a +9b4ab611524d7406 9d2987c1051bd452 +9cf62937439a145a 19c793ec1c918157 +9cf62937439a145a ec0de43959c4c3d1 +a14e721cbc57dbff ec0de43959c4c3d1 +a14e721cbc57dbff eee64002261e2982 +a4efef32dc784f2a e505b6c6b31c35b4 +aa2461d24e7a8851 04357952b0c0d87b +aa2461d24e7a8851 611385f7b64b0554 +aa2461d24e7a8851 739f1d303eeede4d +aa2461d24e7a8851 8b47f1e49f4fe97f +aaa9fae84306cf73 f35ed9b3e12d8df0 +ac10fac9a065513b 460bdaf26dc5d5de +ac10fac9a065513b e9cf6ea5e928b306 +ad0dba73e3521c4f 44df06c3f986986d +b7e6be8139f55e7c 13adbf4ebad1e85f +b7e6be8139f55e7c 3cbe87361ecb3a6a +b7e6be8139f55e7c 4071785723d70eda +b7e6be8139f55e7c 98ba0f0dffbe06fb +b7e6be8139f55e7c a830eb7f17c96b0c +c3a892980c9f1bc1 235dcef274b4241d +c3a892980c9f1bc1 2ab95b13cd096921 +c3a892980c9f1bc1 3466d2c7fb252da9 +c3a892980c9f1bc1 ab17b21738878ae3 +c625ee5057b83a6e 199e9deea0791910 +ce78e77ecb956ccb 2d7cc82b0233a2ba +cf41f4f0e9d550a8 89d9b8e71cc50e62 +d12cdec8456761f0 20599900215db911 +d3ff93e32f2f20dc 5295c34e7179442e +d4135c17b4835b51 d2d785c5eac5767f +dc9c688b97391b8e 800e7c4f63e66de3 +dc9c688b97391b8e d3ccef3531f7f7ca +dd6fd1212d52a7bd 2cecc0c2d3bc8fb8 +dd6fd1212d52a7bd 35e3e36d6f8bb1b5 +dd6fd1212d52a7bd bf73234069e015cc +dd6fd1212d52a7bd d6c6872f8d53e813 +df3028266d9a6623 2d30b32a6bc599ee +df3028266d9a6623 5c5df1542b5bff8b +df3028266d9a6623 6a7f9ee0809b5334 +df3028266d9a6623 8bbb1dc10b37bc05 +df3028266d9a6623 d0d674eb2c33f483 +df3028266d9a6623 ed068a953918fd2e +e19d5b5cd1e76f1d 120f0a976a6ca107 +e19d5b5cd1e76f1d 3103745341950b5e +e19d5b5cd1e76f1d 44b1330452231fa9 +e19d5b5cd1e76f1d 826d84bf0106f618 +e19d5b5cd1e76f1d cdf3e03c37046bcd +e50deab2af6277c6 dcf9179af9d94365 +e897f9e7b7e7abe1 b64f68523fc22633 +eafff9c848e0ed33 ed626950e6cc09dd +ed8b93820ce36531 1a7ec2fdca342c58 +ed8b93820ce36531 5812b4a5572bfd4f +ed8b93820ce36531 fe2f8ee3742e4e13 +eee444ab1c8d1058 50982dbd53d67424 +eee444ab1c8d1058 70589daf1c863703 +ef8a777c2f151751 02210f9e7fe6ad4c +ef8a777c2f151751 048a4a4a753e5076 +ef8a777c2f151751 140a361ef37048e1 +ef8a777c2f151751 1c1a25b7bd1ffb0c +ef8a777c2f151751 a9ab86ce5a9e0084 +ef8a777c2f151751 d7be276590968fb3 +ef8a777c2f151751 e1b7adb71904c320 +ef8a777c2f151751 fac044187b750b53 +f37dda4622020cfc 921fb80b384ab537 +f3a6513a949ba831 557810e3c0241394 +f3a6513a949ba831 c9276c56710f4233 +f64141b26f258051 34619f350df1d911 +f67c93a064c9e2ff 2cecc0c2d3bc8fb8 +f67c93a064c9e2ff 35e3e36d6f8bb1b5 +f67c93a064c9e2ff bf73234069e015cc +f67c93a064c9e2ff d6c6872f8d53e813 +f9dc2debb451353d c0579baa232152ed +f9f665e0884417d4 39baf2e91789d314 +f9f665e0884417d4 e69295f44bb2da6e +fd1e81f63ebb59b6 19c793ec1c918157 +fd1e81f63ebb59b6 31e59687e92d4493 +fd1e81f63ebb59b6 432321c03163b463 +fd1e81f63ebb59b6 5655df85fdb4995c +fd1e81f63ebb59b6 ec0de43959c4c3d1 diff --git a/dm/games/01007EF00011E000/README.md b/dm/games/01007EF00011E000/README.md index 4f39eaa..9a4eb51 100644 --- a/dm/games/01007EF00011E000/README.md +++ b/dm/games/01007EF00011E000/README.md @@ -112,6 +112,7 @@ Used at `DM_FPS_TARGET=0` and for the interval the framework keeps while the VFR General host changes first needed here; each applies to every game: - **Pipeline keys omit disabled blend factors** (`pso_key_norm`): [Pipeline keys omit unused colour state](../../../docs/reference/shaders.md#pipeline-keys-omit-unused-colour-state). +- **Resolved: GPU readback temporaries retained by a game thread's outer pool** (`mtl_read_texels`): [GPU readback without an autorelease pool](../../../docs/reference/memory.md#gpu-readback-without-an-autorelease-pool). Ported from MacNativeGames; checked here with a synthetic Metal readback lifetime test, without a new gameplay memory measurement. - **The host frame step in whole vsyncs** (`nvn_fstep.h`, `fstep_tick`, `FRAME_STEP` stubs, `fps_target`), explained in full under [Frame rate](#the-frame-step-current); its probes (`nvn_fstep_gpu_slowed`, `nvn_fstep_same`): [development.md, Power](../../../docs/development.md#power). - **Real GPU timestamps per segment** (`gpu_timestamps`, `rep_ticks`), **submissions in segments** (`sub_cut`), **deferred texture binds** (`bh_fresh`), **duplicate colour slots** (`rt_dup_slot`), **cube textures as 2D arrays** (`tex_fit`), **compressed render targets through shadows** (`bcrt_attach` / `bcrt_flush`), **pool lookups** (`pool_find`), **memory fixes** (`defer_drain`'s pool, `g_ghost`, growing pool table), **`nvnTextureCompare` with the channel mapping**, **first-use fill of flushed storage** (`nvn_flushed`, `tex_init_fill`), **partial-cover uniform shadows** (`ubp_get`, `cb_extent.tsv`): [GPU timestamps per submission segment](../../../docs/reference/frame-rate.md#gpu-timestamps-per-submission-segment), [Submissions handed to the GPU in segments](../../../docs/reference/frame-rate.md#submissions-handed-to-the-gpu-in-segments), [Deferred texture binds read at execution](../../../docs/reference/shaders.md#deferred-texture-binds-read-at-execution), [The same texture attached at two colour slots](../../../docs/reference/rendering.md#the-same-texture-attached-at-two-colour-slots), [Cube textures held as 6-layer 2D arrays](../../../docs/reference/rendering.md#cube-textures-held-as-6-layer-2d-arrays), [Compressed textures as render targets through an uncompressed shadow](../../../docs/reference/rendering.md#compressed-textures-as-render-targets-through-an-uncompressed-shadow), [Address to memory pool lookups by binary search](../../../docs/reference/frame-rate.md#address-to-memory-pool-lookups-by-binary-search), [Deferred-release drain without an autorelease pool](../../../docs/reference/memory.md#deferred-release-drain-without-an-autorelease-pool), [Dead texture serials kept in a ghost table](../../../docs/reference/nvn-objects.md#dead-texture-serials-kept-in-a-ghost-table), [Memory-pool tracking table grows in blocks](../../../docs/reference/nvn-objects.md#memory-pool-tracking-table-grows-in-blocks), [`nvnTextureCompare` compares the channel mapping](../../../docs/reference/rendering.md#nvntexturecompare-compares-the-channel-mapping), [First-use fill of storage the game flushed](../../../docs/reference/nvn-objects.md#first-use-fill-of-storage-the-game-flushed), [Partial-cover uniform shadows keep passes open](../../../docs/reference/rendering.md#partial-cover-uniform-shadows-keep-passes-open). - **Display-link waits per present interval** (`mtl_link_wait_tick`): [Frames of a dynamic present interval held on screen and at the acquire](../../../docs/reference/frame-rate.md#frames-of-a-dynamic-present-interval-held-on-screen-and-at-the-acquire). diff --git a/dm/games/0100F2C0115B6000/README.md b/dm/games/0100F2C0115B6000/README.md index 372ca05..7055d03 100644 --- a/dm/games/0100F2C0115B6000/README.md +++ b/dm/games/0100F2C0115B6000/README.md @@ -26,6 +26,7 @@ Update 1.4.3 (v720896) over the base game, SDK 20.5.22. Nintendo EPD's engine (s ## Memory +- **Current (issue #147): confirmed transient upload-memory bug on the Mac; iOS validation pending.** With update 1.4.3, the corrected issue #70 SDK module, shader chain `dec5ee141a9f` and 30 fps, walking from the Great Sky Island save produced 10.70 GiB peak footprint after about 474 s; a shorter repeat reached 12.81 GiB after about 150 s. Live staging peaked at 4.48 / 5.82 GiB and the open batch at 4.41 / 5.75 GiB, then memory fell back near 4 GiB. The shorter burst logged 3,084.2 MB of uploads in one frame. A temporary 50 ms sampler and the kernel footprint-peak ledger caught peaks missed by the periodic reports and the 8 GiB sampled watchdog. With [bounded batches and upload-completion retirement](../../../docs/reference/memory.md#upload-staging-grows-within-one-rendering-submission), the 220 s shortened route with deferred encoding enabled peaked at 4.24 GiB footprint and 216 MiB live staging, without GPU errors. A 520 s replay of the original input timings peaked at 4.23 GiB / 200 MiB, but landed differently and reached Game Over earlier, so it is not an equivalent traversal comparison. These local results do not establish the cause of the reporter's full-device reboot or rule out longer-term cache growth; the requested save/route, full logs and Jetsam/panic reports are still needed for that comparison. - The NPDM declares 24 MB of system resource, so `IsVirtualAddressMemoryEnabled` is true and the game's memory lives in the alias region. `nninitStartup` also calls `SetHeapSize(0x86000000)` (2.1 GB of the pool as its heap; a 2 GB heap region aborts there with 0xd401). What it can map is bounded by the pool less the heap (1.1 GB). - **Resolved (issue #70):** the resource heaps are address regions in the alias region (2.375 GB here, 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. The game then took a memory shortage: its frame check (`main+0x1fd32ec`) logs `MSF`, then `R` and `Rf` (reason bits of one resource heap's object), and plays the memory-shortage Blood Moon, which puts Link back at the start. With the node area cut to 62 MB (as Fire Emblem Engage's, Hollow Knight: Silksong's and other builds have it) the high-water mark is +1.75 GB, and no shortage came in 3.7 minutes on the island (it came within a minute in 7 of 8 runs without it). @@ -63,6 +64,7 @@ Game-specific findings. The general host changes they led to are listed under [F ## Mac routes - The issue #70 save: `DM_KEYS="20:A*6/4,f2480:LUp~5"` (title, load, jump, steer into the pond). +- The issue #147 short streaming route uses a fresh copy of the issue #70 save and its corrected SDK module: `DM_FPS_TARGET=30 DM_KEYS="20:A*6/4,f2480:LUp~9,f4000:LUp~20,f4700:LRight~5,f5000:LUp~20"`, run for at least 220 s. This is a fall/respawn stress route, ending at Game Over in both short comparison runs; it is not a long surviving traversal test. The movement timings are repeatable, but the upload burst varies with streaming and CPU/GPU timing, so the deterministic staging regression complements it. - The in-cave autosave (`run/save`): keys `f2500:A,f2800:A,f3100:A,f3600:A`, shot at frame 4500. `DM_DETCLOCK` cannot be used: the game stops at frame 1 waiting for guest time. ## Found on this game @@ -70,6 +72,8 @@ Game-specific findings. The general host changes they led to are listed under [F General host changes first needed here; each applies to every game: - **Pipeline keys omit absent-target blend and mask state**, and old prewarm rows are normalized and deduplicated: [Pipeline keys omit unused colour state](../../../docs/reference/shaders.md#pipeline-keys-omit-unused-colour-state). +- **Bounded upload staging and retirement on upload completion:** [Upload staging grows within one rendering submission](../../../docs/reference/memory.md#upload-staging-grows-within-one-rendering-submission). +- **Resolved: registry lock priority inversion** (`g_rmu`): [Registry lock priority inheritance](../../../docs/reference/frame-rate.md#registry-lock-priority-inheritance). Ported from MacNativeGames; checked here through the Metal registry view tests, without a new gameplay contention measurement. - **Conditional rendering** evaluated at replay (`OP_RENDER_COND`, profile key `render_cond`) and **CPU-written textures with no flush** (`tex_dirty_upload`, `ft_add`): [Conditional rendering evaluated at replay](../../../docs/reference/rendering.md#conditional-rendering-evaluated-at-replay), [CPU writes with no flush, caught by page protection](../../../docs/reference/nvn-objects.md#cpu-writes-with-no-flush-caught-by-page-protection-tex_dirty_upload). - **Mid-pass, partial-mask and partial-scissor clears** drawn as a triangle (`clear_color_tri` / `clear_ds_tri`), **8-bit indices widened on the GPU** when written in the same submission (`wide_run`), **`ReadTexels` from guest memory on any thread** with `nvn_ft_poll` marking textures written after first use, **attachment-less passes** (`bare_dims`): same section. - **Rewriter:** out-of-line atomic retry blocks (`retry_block` / `site_retry_window`, 8 sites) and jump tables relative to an `adr` (`jump_adr_offsets`, `main+0x393bb0`): [Out-of-line retry blocks of atomic updates](../../../docs/reference/rewriter.md#out-of-line-retry-blocks-of-atomic-updates), [Offset tables relative to an adr](../../../docs/reference/rewriter.md#offset-tables-relative-to-an-adr). diff --git a/dm/games/README.md b/dm/games/README.md index 05fb4ed..6ab8539 100644 --- a/dm/games/README.md +++ b/dm/games/README.md @@ -85,6 +85,7 @@ One row per profile; the notes are each profile's `README.md`. | [Pikmin 4](0100B7C00933A000/README.md) | `0100B7C00933A000` | 1.1.1 | 10.4.8 | Unreal Engine 4.26.2 | NVN | | [Pokémon Legends: Z-A](0100F43008C44000/README.md) | `0100F43008C44000` | 2.0.2 | 19.3.11 | Game Freak's Trinity engine | NVN | | [Pokémon Sword](0100ABF008968000/README.md) | `0100ABF008968000` | 1.3.2 | 7.3.2 | Game Freak's engine (before Trinity) | NVN | +| [Portal](01007BB017812000/README.md) | `01007BB017812000` | 1.0.3 | 13.3.0 | Valve's Source engine (28 NROs) | NVN | | [Raging Loop](010020400E1C2000/README.md) | `010020400E1C2000` | 1.0.0 (base) | 7.3.2 | Unity 2018.3.10f1 IL2CPP | NVN | | [Rhythm Heaven Groove](0100D9F01D474000/README.md) | `0100D9F01D474000` | 1.0.0 (base) | 21.4.0 | Nintendo's own 2D engine | NVN | | [ROMANCE OF THE THREE KINGDOMS 8 REMAKE](0100C2A01A730000/README.md) | `0100C2A01A730000` | 1.1.3 | 17.5.3 | Koei Tecmo's KTGL | NVN | @@ -161,7 +162,7 @@ These are not profile keys: the tools read them from the game files each time, b 1. **Survey.** Run `scripts/build_ipa_from_nsp.py` on the game's NSP folder. Without a profile it extracts only the code modules and a RomFS list, then writes `/_survey/survey.md`. The survey covers modules and build IDs, SDK, engine and graphics API markers, RomFS containers, the rewrite-site census, and imports compared with the host hooks. Unless told `--survey-only`, the script then writes a **provisional profile** to `<NSP folder>/<title ID>_build/profile/<title ID>/` (outside the repository) and carries on to a best-effort IPA. `dm/games/profile_guess.py` fills its `game.tsv` from the game files, each key under the reason for it (the survey lists them too): - identity and name, the build row it found, the largest RomFS file as `romfs_marker`, big file types in `zip_store`, `shader_groups` by RomFS top-level folder; - - `modules`: a subsdk is loaded only when `main` imports something from it that the host does not supply itself. Under the Vulkan layer the Vulkan loader and GLSLC are dropped; movie modules and a GLSLC that `main` calls stay. NVIDIA's OpenGL and Vulkan drivers are never loaded: they program the GPU through NvRm, which the host does not have (Absolum imports EGL for SDL3's GL support and renders with NVN); + - `modules`: a subsdk is loaded only when `main` imports something from it that the host does not supply itself, or, once the rewrite stage has collected the game's NROs, when one of them does (`nro_modules`: Portal's Source engine NROs use the movie module and GLSLC, which its `main` does not). Under the Vulkan layer the Vulkan loader and GLSLC are dropped; movie modules and a GLSLC that `main` calls stay. NVIDIA's OpenGL and Vulkan drivers are never loaded: they program the GPU through NvRm, which the host does not have (Absolum imports EGL for SDL3's GL support and renders with NVN); - the memory partition. Without a system resource in the NPDM: the 4 GB layout, its heap region the pool − 64 MB − the bss of giant modules (Metroid Dread: 0x90000000). When `main` never sets a heap: a 1 GB block (Super Mario Galaxy). With a system resource: Engage's 5 GB block with the hardware pool, which the heap check corrects when the game sets a heap (without one the game maps everything in the alias region itself: Hollow Knight: Silksong, Absolum, Pokémon Legends: Z-A); - Engine presets: Hagi (`hagi.config`) gets `op_mode handheld` and `pad_layout full`. diff --git a/dm/games/profile_guess.py b/dm/games/profile_guess.py index 35c9dc9..faac15d 100644 --- a/dm/games/profile_guess.py +++ b/dm/games/profile_guess.py @@ -130,9 +130,8 @@ def guess(F, hooks=None): # Code modules: a subsdk main takes nothing from (nothing the host does not supply itself) is not loaded: NVIDIA's OpenGL / Vulkan drivers and the Vulkan loader under a Vulkan game # (the host's Vulkan layer answers main's vk* calls, and a loaded driver would take them first), GLSLC and nn::pcm under Super Mario Galaxy. The movie modules stay. # NVIDIA's OpenGL and Vulkan drivers are never loaded: they program the GPU through NvRm, which the host does not have. Absolum (FNA3D on NVN) imports EGL for SDL3's GL support, never used - order = lambda m: (m != 'main', m == 'sdk', int(m[6:]) if m.startswith('subsdk') and m[6:].isdigit() else 0) keep, drop = [], [] - for name in sorted((m for m in mods if m != 'rtld'), key=order): + for name in sorted((m for m in mods if m != 'rtld'), key=load_order): if name in ('main', 'sdk') or ((imp & mods[name]['exports']) - hooks and mods[name]['kind'] not in GPU_DRIVERS): keep.append(name) else: drop.append(f'{name} ({mods[name]["kind"]})') G['modules'] = (' '.join(keep), f'not loaded, main takes nothing from them the host does not supply: {", ".join(drop)}' if drop else 'every module main imports from') @@ -169,8 +168,36 @@ HEAP_API = '_ZN2nn2os17SetMemoryHeapSizeEm' GPU_DRIVERS = {'NintendoSDK_OpenGL', 'NintendoSDK_Vulkan'} +def load_order(m): # the modules' load order on Horizon: main, the subsdks by number, sdk + return (m != 'main', m == 'sdk', int(m[6:]) if m.startswith('subsdk') and m[6:].isdigit() else 0) + + +def nro_modules(exefs, nros, modules, hooks=None): + """The subsdks a provisional profile leaves out (main takes nothing from them) that the NROs the game loads at run time import from, {module: kind}: guess() sees only main's + imports, and the NROs are in the RomFS, which the survey does not extract. Portal (Source engine, 28 NROs) plays its movies in video_nx.nro through NintendoSDK_movie (subsdk2) + and initializes GLSLC (subsdk0) from materialsystem.nro and video_nx.nro; left out, movie::Extractor's constructor was a stub and the first movie crashed. + A symbol a loaded module or another NRO defines, or the host supplies, needs nothing; NVIDIA's GPU drivers stay out as in guess()""" + exefs, hooks = Path(exefs), host_hooks() if hooks is None else hooks + want, have = set(), set() + for p in nros.values(): + for s, defined, bind in nso_syms.dynsyms(Path(p).read_bytes()): + if not defined: want.add(s) + elif bind in (1, 2): have.add(s) + exp = {} + for p in sorted(exefs.iterdir()): + with open(p, 'rb') as f: + if f.read(4) != b'NSO0': continue + img = nso(p)[0] + exp[p.name] = {'exports': {s for s, defined, bind in nso_syms.dynsyms(img) if defined and bind in (1, 2)}, + 'comps': {c.decode().rsplit('-', 1)[0] for c in re.findall(rb'NintendoSDK_(?!nnSdk)\w+-[0-9_]+', img)}} + for m in modules: have |= exp.get(m, {}).get('exports', set()) + want -= have | hooks + return {m: kind(e) for m, e in sorted(exp.items(), key=lambda x: load_order(x[0])) + if m not in modules and m != 'rtld' and want & e['exports'] and kind(e) not in GPU_DRIVERS} + + # ---- Shader sources: decided from the game files at every build, not kept in the profile (scripts/build_ipa_from_nsp.py; shader_split.py reads what those steps write) ---- -def shader_sources(imports, glsl_src, glslc_mods, spirv, unreal_paks, trinity_packs=0, legacy_paks=0, gl_names=0, gl_src=0, gl_files=0, oodle_pkz=0): +def shader_sources(imports, glsl_src, glslc_mods, spirv, unreal_paks, trinity_packs=0, legacy_paks=0, gl_names=0, gl_src=0, gl_files=0, oodle_pkz=0, nro_imports=frozenset()): """{name: (value, reason)}: graphics (vulkan when main imports Vulkan functions and not nvnBootstrapLoader; gl when it imports eglCreateWindowSurface and names glShaderSource, so it compiles GLSL through OpenGL (gl_names: how often main has that name; Absolum imports the same EGL functions for SDL3 but renders with NVN and names no GL function); else nvn; vkGetInstanceProcAddr alone is SDL's Vulkan loader, which Balatro's LÖVE links and never uses), vk_capture (a Vulkan game without SPIR-V files generates its shaders at run time), gl_capture (an OpenGL game with no @@ -181,14 +208,20 @@ def shader_sources(imports, glsl_src, glslc_mods, spirv, unreal_paks, trinity_pa unreal_paks counts the paks with shader code archives, legacy_paks those with inline shaders (uepak.inline_shaders). Checked against every profile: Hades and Super Mario Galaxy Vulkan, Galaxy alone with no SPIR-V file, Three Houses alone compiling shaders (13 Sentinels holds 106 GLSL sources but does not import glslcCompile; Captain Toad: Treasure Tracker imports it with agl's GLSL sources, but no module defines it), Octopath Traveler 0 alone with version 11 Unreal paks, DRAGON QUEST XI S alone with legacy ones (OCTOPATH TRAVELER II's version 11 pak has inline shaders too), Pokémon Legends: Z-A alone with Trinity packs, Digimon Story Cyber Sleuth and Balatro alone with OpenGL (of the 23 games - extracted; Hades and both Galaxies import 99 or 100 Vulkan functions, Balatro only vkGetInstanceProcAddr), Balatro alone composing its GLSL""" + extracted; Hades and both Galaxies import 99 or 100 Vulkan functions, Balatro only vkGetInstanceProcAddr), Balatro alone composing its GLSL. + nro_imports: what the NROs the game loads at run time import. Portal's main is SDL with its EGL backend (it names glShaderSource for SDL's GL renderer) and renders nothing: its Source engine + NROs draw with NVN (materialsystem.nro and video_nx.nro import nvnBootstrapLoader) and call glslcCompile; the GLSLC outputs it ships (nxcontent/nvsb in game.zip) are read by the split""" vk = sum(1 for s in imports if re.match(r'vk[A-Z]', s) and s != 'vkGetInstanceProcAddr'); vulkan = bool(vk) and 'nvnBootstrapLoader' not in imports - gl = not vulkan and 'eglCreateWindowSurface' in imports and bool(gl_names) + nro_nvn = 'nvnBootstrapLoader' in nro_imports + gl = not vulkan and not nro_nvn and 'eglCreateWindowSurface' in imports and bool(gl_names) + nro_glslc = not vulkan and not gl and 'glslcCompile' in nro_imports and bool(glslc_mods) return {'graphics': ('vulkan', f'main imports {vk} Vulkan functions and not nvnBootstrapLoader: the host\'s Vulkan layer (dm/gfx/vk/hle_vk.m)') if vulkan else - ('gl', 'main imports EGL and names glShaderSource: it renders with OpenGL and compiles GLSL, the host\'s GL layer (dm/gfx/gl/hle_gl.m)') if gl else ('nvn', 'main imports nvnBootstrapLoader'), + ('gl', 'main imports EGL and names glShaderSource: it renders with OpenGL and compiles GLSL, the host\'s GL layer (dm/gfx/gl/hle_gl.m)') if gl else + ('nvn', 'the NROs the game loads import nvnBootstrapLoader' if nro_nvn and 'nvnBootstrapLoader' not in imports else 'main imports nvnBootstrapLoader'), 'vk_capture': (vulkan and not spirv, 'no SPIR-V file in the RomFS: the game generates its shaders at run time (Super Mario Galaxy), captured by booting it on the Mac'), 'gl_capture': (gl and not gl_src and not gl_files, 'no GLSL source in the RomFS or main: the game puts its shaders together at run time (LÖVE: Balatro), captured by booting it on the Mac'), - 'glslc_capture': (not vulkan and not gl and bool(glsl_src) and 'glslcCompile' in imports and bool(glslc_mods), + 'glslc_capture': (not vulkan and not gl and bool(glsl_src) and 'glslcCompile' in imports and bool(glslc_mods) or nro_glslc, + f'the NROs the game loads call glslcCompile in {", ".join(glslc_mods)}: shaders compiled at run time (Portal), captured by booting it on the Mac' if nro_glslc and not (glsl_src and 'glslcCompile' in imports) else f'main holds {glsl_src} GLSL sources and calls glslcCompile in {", ".join(glslc_mods) or "no module it loads"}: shaders compiled at run time (Fire Emblem: Three Houses), captured by booting it on the Mac'), 'ue_decode': (not vulkan and not gl and bool(unreal_paks), f'{unreal_paks} Unreal pak(s): the shaders are Oodle-compressed in their shader code archives (Octopath Traveler 0), decoded by booting the game on the Mac'), 'oodle_packs': (not vulkan and not gl and bool(trinity_packs or oodle_pkz), (f'{trinity_packs} Trinity pack file(s) (ONEPACK, arc/data.trpfs: Pokémon Legends: Z-A)' if trinity_packs else @@ -197,12 +230,25 @@ def shader_sources(imports, glsl_src, glslc_mods, spirv, unreal_paks, trinity_pa 'version 11 without shader code archives, UE 4.27: OCTOPATH TRAVELER II, Zlib): the shader split reads them file by file')} +def romfs_nro_imports(romfs): # the names the loose NROs in a RomFS import (the modules a game loads at run time with nn::ro: Portal's 28 Source engine modules in nro/) + out = set() + for d, _, names in os.walk(romfs): + for n in names: + if n.endswith('.nro'): + b = Path(d, n).read_bytes() + if b[0x10:0x14] == b'NRO0': out |= {s for s, defined, _ in nso_syms.dynsyms(b) if not defined} + return out + + def build_sources(exefs, romfs, modules): """shader_sources from an extraction (its ExeFS and RomFS folders) and the modules the profile loads""" exefs = Path(exefs); main = nso(exefs / 'main')[0] imports = {s for s, defined, _ in nso_syms.dynsyms(main) if not defined}; glsl_src = main.count(b'#version '); gl_names = main.count(b'glShaderSource'); gl_src = len(re.findall(GL_SRC, main)) - glslc_mods = [m for m in modules if m != 'main' and glsl_src and 'glslcCompile' in imports and (exefs / m).exists() - and any(s == 'glslcCompile' and defined for s, defined, _ in nso_syms.dynsyms(nso(exefs / m)[0]))] + nro_imp = romfs_nro_imports(romfs) + defines = lambda m: (exefs / m).exists() and any(s == 'glslcCompile' and defined for s, defined, _ in nso_syms.dynsyms(nso(exefs / m)[0])) + glslc_mods = [m for m in modules if m != 'main' and glsl_src and 'glslcCompile' in imports and defines(m)] + if 'glslcCompile' in nro_imp: # an NRO compiles (Portal's materialsystem.nro): the GLSLC module is one nro_modules adds to a provisional profile, so any of the ExeFS's counts + glslc_mods = sorted({*glslc_mods, *(p.name for p in exefs.iterdir() if p.name.startswith('subsdk') and defines(p.name))}, key=load_order) spirv = 0 if any(re.match(r'vk[A-Z]', s) for s in imports): # only a Vulkan game's RomFS is read spirv = sum(1 for _, _, head in romfs_files(romfs) if head.startswith(bytes.fromhex('03022307'))) @@ -221,7 +267,7 @@ def build_sources(exefs, romfs, modules): h = f.read(0x20) if h[:4] == b'pkzl': cnt, eo, nl = struct.unpack_from('<III', h, 0x10); f.seek(eo + 0x20 * cnt); oodle_pkz += b'OodleKraken\0' in f.read(nl) - return shader_sources(imports, glsl_src, glslc_mods, spirv, len(uepak.unreal_paks(str(romfs), inline=False)), trinity, len(uepak.unreal_paks(str(romfs), inline=True)), gl_names, gl_src, gl_files, oodle_pkz) + return shader_sources(imports, glsl_src, glslc_mods, spirv, len(uepak.unreal_paks(str(romfs), inline=False)), trinity, len(uepak.unreal_paks(str(romfs), inline=True)), gl_names, gl_src, gl_files, oodle_pkz, nro_imp) def va_heap(sysres, imports): # virtual address memory (an NPDM system resource) and a heap: the alias / heap split is a starting point the heap check corrects (Wonder, Tears of the Kingdom; Engage's pool is its own measurement) @@ -296,6 +342,12 @@ def find_extraction(root, g): # (ExeFS, RomFS manifest or folder) for a profil return None +def loose_nros(root, g, ex): # {name: path} of the loose NROs in a profile's RomFS: the extraction's own folder, else an NSP script extraction of the packed version beside its survey + roms = [Path(ex[1])] if Path(ex[1]).is_dir() else [d / f'{g.id}_build/extract' / g.get('package', '') / 'RomFS' for d in sorted(Path(root).iterdir())] + rom = next((r for r in roms if r.is_dir()), None) + return {p.name: p for p in sorted(rom.rglob('*.nro')) if p.is_file()} if rom else {} + + def check(root): sys.path.insert(0, str(HERE)); import game hooks, bad = host_hooks(), 0 @@ -304,6 +356,9 @@ def check(root): ex = find_extraction(root, g) if not ex: print(f'{g.id} {g.get("name")}: no extraction under {root}'); continue F = facts_of(*ex); G = guess(F, hooks) + nros = loose_nros(root, g, ex) # what the NSP script adds once the rewrite stage has collected the NROs (a survey's RomFS manifest has none to read) + if nros and (add := nro_modules(ex[0], nros, G['modules'][0].split(), hooks)): + G['modules'] = (' '.join(sorted({*G['modules'][0].split(), *add}, key=load_order)), '') diff, unknown, later = [], [], [] for k in list(DEFAULTS) + ['modules', 'mem_reserve', 'mem_alias', 'mem_stack', 'app_pool']: want, got = g.get(k, DEFAULTS.get(k, '')), G.get(k, (DEFAULTS.get(k, ''),))[0] diff --git a/dm/gfx/nvn/hle_nvn.c b/dm/gfx/nvn/hle_nvn.c index adf4ac7..208cb05 100644 --- a/dm/gfx/nvn/hle_nvn.c +++ b/dm/gfx/nvn/hle_nvn.c @@ -936,13 +936,19 @@ NVN(nvnCommandBufferBindVertexStreamState, void, NVNcommandBuffer *c, int n, con } NVN(nvnCommandBufferBindVertexBuffers, void, NVNcommandBuffer *c, int first, int n, const NVNbufferRange *rg) { if (n < 0 || first + n > 16) { LOG("[RED] BindVertexBuffers first=%d n=%d", first, n); return; } + if (n == 1 && (unsigned)first < 16 && rg[0].size <= UINT32_MAX) { + *REC(c, OP_BIND_VBUF1, p_bind_vbuf1) = (p_bind_vbuf1){(uint32_t)first, (uint32_t)rg[0].size, rg[0].address}; return; + } p_bind_vbuf *r = RECN(c, OP_BIND_VBUF, p_bind_vbuf, r, n); r->first = first; r->n = n; memcpy(r->r, rg, sizeof *rg * n); } NVN(nvnCommandBufferBindUniformBuffer, void, NVNcommandBuffer *c, NVNshaderStage st, int i, NVNbufferAddress a, size_t n) { + if ((unsigned)st < 6 && (unsigned)i < 16 && n <= UINT32_MAX) { + *REC(c, OP_BIND_UBO_SMALL, p_bind_ubo_small) = (p_bind_ubo_small){(uint16_t)st, (uint16_t)i, (uint32_t)n, a}; return; + } *REC(c, OP_BIND_UBO, p_bind_buf) = (p_bind_buf){st, i, a, n}; } NVN(nvnCommandBufferBindUniformBuffers, void, NVNcommandBuffer *c, NVNshaderStage st, int first, int n, const NVNbufferRange *rg) { - for (int i = 0; i < n; i++) *REC(c, OP_BIND_UBO, p_bind_buf) = (p_bind_buf){st, first + i, rg[i].address, rg[i].size}; + for (int i = 0; i < n; i++) h_nvnCommandBufferBindUniformBuffer(c, st, first + i, rg[i].address, rg[i].size); } NVN(nvnCommandBufferBindStorageBuffer, void, NVNcommandBuffer *c, NVNshaderStage st, int i, NVNbufferAddress a, size_t n) { *REC(c, OP_BIND_SSBO, p_bind_buf) = (p_bind_buf){st, i, a, n}; diff --git a/dm/gfx/nvn/nvn_mtl.m b/dm/gfx/nvn/nvn_mtl.m index 76dec77..b688796 100644 --- a/dm/gfx/nvn/nvn_mtl.m +++ b/dm/gfx/nvn/nvn_mtl.m @@ -919,7 +919,7 @@ static id<MTLBuffer> g_ubs_buf; static size_t g_ubs_off, g_ubs_size; // made of them alone, latest first per byte (exactly what the GPU would read, whatever earlier frames still have in flight); when they cover only part of it, the rest comes from memory (ubp_get). // The real copies are encoded in order when the pass ends (rp_end), so later passes and the CPU see them as before. The pass still ends before a draw that would need more: a partly covered binding // whose rest memory cannot give, or a read of the deferred data through a vertex, index, indirect or storage buffer (ubd_conflict). Off in TBDR mode (it reorders passes). DM_NVN_UB_DEFER=0 turns it off -static size_t arena_alloc(size_t n, id<MTLBuffer> *buf); static inline void args_dirty_all(void); static void ubd_dirty(uint64_t lo, uint64_t hi); +static size_t arena_alloc(size_t n, __strong id<MTLBuffer> *buf); static inline void args_dirty_all(void); static void ubd_dirty(uint64_t lo, uint64_t hi); typedef struct { uint64_t lo, hi, gpu; size_t off; __unsafe_unretained id<MTLBuffer> buf; } ubd_ent; // data at buf + off (the ubs ring; a ring block replaced meanwhile lives until this submission completes) enum { UBD_MAX = 512 }; static ubd_ent g_ubd[UBD_MAX]; static int g_nubd; @@ -1644,6 +1644,11 @@ static NSMutableArray *g_up_texs; // Target textures in the batch: before the static id<MTLBuffer> g_up_block; static size_t g_up_block_off, g_up_block_size; static NSMutableArray *g_up_keep; +// Limit overflow staging per batch. The three upload slots bound in-flight +// batches; up_begin publishes queued commits before waiting for a slot. A single +// texture larger than the budget gets its own block (uploads are not tiled here). +static size_t g_up_staging_bytes; +#define UP_BATCH_BYTES (128u * 1024u * 1024u) // The upload command buffer was discarded by the system (dropped along during GPU error recovery; the first frame on entering Travincal drops one every run): the objects of this batch of textures are built and the timestamps updated, but the content was never written in, @@ -1669,6 +1674,12 @@ static void up_flush(void) { if (b) { m4_end(b); g_up_ce = nil; } id<MTL4CommandBuffer> cb = g_up_cb; unsigned slot = (g_up_seq - 1) % 3; + // Only this upload batch uses its overflow staging. Retire it on upload + // completion, even when the main rendering submission is still being built. + NSMutableArray *staging = g_up_keep; + if (g_up_block && g_up_block != g_up_slot_blk[slot]) [staging addObject:g_up_block]; + g_up_keep = nil; g_up_block = nil; + g_up_block_off = g_up_block_size = g_up_staging_bytes = 0; NSMutableArray *dsts = [NSMutableArray arrayWithCapacity:(NSUInteger)g_nup_blits]; for (int i = 0; i < g_nup_blits; i++) if (g_up_blits[i].dst) [dsts addObject:g_up_blits[i].dst]; static int lost_at = -2; static uint64_t batch_n; if (lost_at == -2) { const char *e = getenv("DM_TEST_UP_LOST"); lost_at = e ? atoi(e) : -1; } int fake_lost = dsts.count && lost_at > 0 && (int)++batch_n == lost_at; MTL4CommitOptions *o = [MTL4CommitOptions new]; @@ -1682,6 +1693,11 @@ static void up_flush(void) { } double d = f.GPUEndTime - f.GPUStartTime; if (d > 0) atomic_fetch_add(&g_upload_gpu_ns, (uint64_t)(d * 1e9)); + @autoreleasepool { + for (id<MTLResource> obj in staging) res_remove(obj); + [staging removeAllObjects]; + res_commit(); + } atomic_store(&g_up_alloc_busy[slot], 0); atomic_fetch_sub(&g_up_pending, 1); up_wake(); @@ -1692,9 +1708,7 @@ static void up_flush(void) { // Outside a submission nothing else hands the encode thread its current block until the next submission, so a batch flushed on the spot stayed a record and every wait for it // (nvnQueueFinish on any thread, up_begin on another) waited for that submission: hand it over now (from PR #141, which found the nvnQueueFinish half) if (!g_cb) rec_publish(); - g_up_cb = nil; if (g_up_block) { if (g_up_block != g_up_slot_blk[slot]) defer_obj_release(g_up_block); g_up_block = nil; } g_up_block_off = g_up_block_size = 0; // the slot's own block stays (UP_SLOT_BLOCK) - for (id obj in g_up_keep) defer_obj_release(obj); - [g_up_keep removeAllObjects]; + g_up_cb = nil; up_diag(); g_nup_blits = 0; [g_up_texs removeAllObjects]; @@ -1723,7 +1737,9 @@ static void up_begin(void) { // Diagnostic readout (low frequency; printed only when the block switch count changed and ≥ 1 second since the last line, at most 200 lines per game): how many times an upload batch ran out of staging block and switched blocks midway; GPU allocated, residency set object count, deferred-release queue length. // If the old block is not retired on a switch it stays in the residency set and can never be freed, and "GPU allocated" climbs one block at a time with the switch count static uint64_t g_up_blocks_new, g_up_swaps, g_up_swap_bytes; static NSHashTable *g_up_old; // g_up_old: old blocks switched out (weak references); the diagnostic line counts how many are still alive -static void up_diag(void) { +static void up_diag(void) { @autoreleasepool { + // Weak-table enumeration autoreleases the buffers it finds. Do not keep + // completed batches alive until the whole rendering submission ends. // Print a line when the block switch count changes; then one more line 5 seconds later (that batch's commit is long done and deferred retirement drained, so "GPU allocated" is the settled number) static uint64_t said_swaps = ~0ull, last_ns; static int lines, follow; uint64_t now = clock_gettime_nsec_np(CLOCK_MONOTONIC); int chg = g_up_swaps != said_swaps, fol = follow && now - last_ns >= 5000000000ull; if ((!chg && !fol) || lines >= 200) return; @@ -1733,25 +1749,35 @@ static void up_diag(void) { pthread_mutex_lock(&g_deferred_mu); unsigned long ndef = (unsigned long)g_deferred.count; pthread_mutex_unlock(&g_deferred_mu); MLOG("upload staging blocks%s: %llu created in total, swapped within a batch %llu times (%llu MB of old blocks swapped out); swapped-out blocks still alive now %lu, %llu MB, still in the residency set %lu; GPU allocated %llu MB, residency set %lu objects, deferred-release queue %lu", fol && !chg ? " (settled)" : "", (unsigned long long)g_up_blocks_new, (unsigned long long)g_up_swaps, (unsigned long long)(g_up_swap_bytes >> 20), alive, (unsigned long long)(abytes >> 20), inres, (unsigned long long)(g_dev.currentAllocatedSize >> 20), nres, ndef); -} +} } static id<MTLBuffer> up_alloc(size_t n, size_t *off) { size_t need = (n + 255u) & ~((size_t)255u); + size_t extra = MAX(need, UP_BLOCK_SIZE); + // This call starts one texture's upload: all prior deswizzles and copies are + // complete records, so the batch can be split without ending the render pass. + if (g_up_staging_bytes && (!g_up_block || need > g_up_block_size - g_up_block_off) + && extra > UP_BATCH_BYTES - MIN(g_up_staging_bytes, UP_BATCH_BYTES)) { + up_flush(); + up_begin(); + } if (!g_up_block && need <= UP_BLOCK_SIZE) { // first allocation of the batch: the slot's own block, grown to the next power of two that holds it (a movie frame re-uploaded every frame, 8 MB in Link's Awakening) __strong id<MTLBuffer> *sb = &g_up_slot_blk[g_up_cur_slot]; size_t want = UP_SLOT_BLOCK; while (want < need) want <<= 1; if (*sb && (*sb).length < want) { defer_obj_release(*sb); *sb = nil; } // the slot's previous batch has finished (up_begin waited for it) if (!*sb) { *sb = [g_dev newBufferWithLength:want options:MTLResourceStorageModePrivate]; if (!*sb) MDIE("failed to create the swizzled-upload staging block %#zx", want); res_add(*sb); g_up_blocks_new++; } g_up_block = *sb; g_up_block_size = (*sb).length; g_up_block_off = 0; + g_up_staging_bytes += (*sb).length; } if (!g_up_block || g_up_block_off + need > g_up_block_size) { // Block switch: the old block is still referenced by copy / decompress commands already recorded in this batch and cannot be freed on the spot; hand it to g_up_keep, to be retired later with the other staging after the flush commits (removed from the residency set on retire). // Previously g_up_block was simply replaced with the new block here: nobody retired the old block, yet it stayed in the residency set, so each extra block switch in an upload batch leaked one block (usually 32 MB) of GPU memory. // The old block and the "current block" retired at flush are two objects, so nothing is retired twice - g_up_blocks_new++; if (g_up_block) { g_up_swaps++; g_up_swap_bytes += g_up_block_size; if (!g_up_old) g_up_old = [NSHashTable weakObjectsHashTable]; [g_up_old addObject:g_up_block]; static int leak = -1; if (leak < 0) leak = getenv("DM_TEST_UP_LEAK") != NULL; // test switch DM_TEST_UP_LEAK=1: do not retire the old block, as before the fix (for comparison, to measure the leak) + g_up_blocks_new++; if (g_up_block) { g_up_swaps++; g_up_swap_bytes += g_up_block_size; @autoreleasepool { if (!g_up_old) g_up_old = [NSHashTable weakObjectsHashTable]; [g_up_old addObject:g_up_block]; } static int leak = -1; if (leak < 0) leak = getenv("DM_TEST_UP_LEAK") != NULL; // test switch DM_TEST_UP_LEAK=1: do not retire the old block, as before the fix (for comparison, to measure the leak) if (!leak && g_up_block != g_up_slot_blk[g_up_cur_slot]) { if (!g_up_keep) g_up_keep = [NSMutableArray array]; [g_up_keep addObject:g_up_block]; } } // the slot's block is not retired: it stays for the slot's later batches g_up_block_size = n > UP_BLOCK_SIZE ? need : UP_BLOCK_SIZE; g_up_block = [g_dev newBufferWithLength:g_up_block_size options:MTLResourceStorageModePrivate]; if (!g_up_block) MDIE("failed to create the swizzled-upload staging block %#zx", g_up_block_size); + g_up_staging_bytes += g_up_block_size; res_add(g_up_block); g_up_block_off = 0; } @@ -2897,7 +2923,9 @@ static int clear_rect_zero(int i) { // ================= Shaders / descriptor pool / pipelines / draws ================= -static pthread_mutex_t g_rmu = PTHREAD_MUTEX_INITIALIZER; +// Game threads register textures and samplers, the submit thread encodes arguments: lend a waiting submitter's priority to the owner. +// Measurements: docs/reference/frame-rate.md, Registry lock priority inheritance. +static os_unfair_lock g_rmu = OS_UNFAIR_LOCK_INIT; // ---------- Shader mapping table: Switch control section hash → PC metallib (produced by dm/gfx/tools/shader_map.py) ---------- static NSMutableDictionary<NSString *, NSString *> *g_shmap; static NSString *g_shmap_path, *g_msl_dir; @@ -3372,8 +3400,8 @@ static void vc_put(uint64_t ser, const void *pool, const vc_key *k, id<MTLTextur if (s->view[i]) { defer_obj_unref((__bridge id)s->view[i]); CFRelease(s->view[i]); } s->k[i] = *k; s->view[i] = (void *)CFBridgingRetain(v); g_vc_new++; } -void mtl_vc_drop(uint64_t ser) { pthread_mutex_lock(&g_rmu); vc_set *s = nvn_hm_del(&g_vc, &ser); if (s) vc_set_free(s); pthread_mutex_unlock(&g_rmu); } -void mtl_vc_stats(unsigned *n, uint64_t *hit, uint64_t *made) { pthread_mutex_lock(&g_rmu); *n = g_vc.n; *hit = g_vc_hit; *made = g_vc_new; pthread_mutex_unlock(&g_rmu); } +void mtl_vc_drop(uint64_t ser) { os_unfair_lock_lock(&g_rmu); vc_set *s = nvn_hm_del(&g_vc, &ser); if (s) vc_set_free(s); os_unfair_lock_unlock(&g_rmu); } +void mtl_vc_stats(unsigned *n, uint64_t *hit, uint64_t *made) { os_unfair_lock_lock(&g_rmu); *n = g_vc.n; *hit = g_vc_hit; *made = g_vc_new; os_unfair_lock_unlock(&g_rmu); } static void vc_drop_pool(const void *pool) { // caller holds g_rmu for (uint32_t i = 0; i < g_vc.cap;) { // a deletion shifts later entries back into slot i: look at it again vc_set *s = g_vc.vals[i]; @@ -3414,16 +3442,16 @@ static id<MTLTexture> tbuf_as_2d(int tid, id<MTLTexture> t, uint64_t *rid) { } void mtl_tb2_stats(uint64_t *binds, uint64_t *made, uint64_t *fail) { *binds = g_tb2_binds; *made = g_tb2_made; *fail = g_tb2_fail; } static void treg_drop_pool_views(const void *pool) { - pthread_mutex_lock(&g_rmu); + os_unfair_lock_lock(&g_rmu); vc_drop_pool(pool); for (int i = 0; i < g_ntreg; i++) if (g_treg[i].view && g_treg[i].ok && (const void *)g_treg[i].t.pool == pool) { defer_obj_unref((__bridge id)g_treg[i].view); CFRelease(g_treg[i].view); g_treg[i].view = NULL; g_treg[i].view_id = g_treg[i].view_rid = 0; treg_drop_ar(&g_treg[i]); } - pthread_mutex_unlock(&g_rmu); + os_unfair_lock_unlock(&g_rmu); } void mtl_reg_tex(int tid, const tex_t *t, const view_t *v) { int id_ = tid; if (id_ < 0) return; - pthread_mutex_lock(&g_rmu); + os_unfair_lock_lock(&g_rmu); if (id_ >= g_ntreg) { int n = id_ + 1024; g_treg = realloc(g_treg, n * sizeof *g_treg); memset(g_treg + g_ntreg, 0, (n - g_ntreg) * sizeof *g_treg); g_ntreg = n; } // Registering what the ID already holds changes nothing: its view and the registration generation stay (the lookup still checks the base texture). About 1 in 6 of the // registrations KTGL (Age of Calamity) makes before every bind repeat the ID's texture; the rest cycle textures through its IDs (the view cache below, vc_get) @@ -3437,7 +3465,7 @@ void mtl_reg_tex(int tid, const tex_t *t, const view_t *v) { rr->ok = t != NULL; if (t) rr->t = *t; rr->has_v = v != NULL; if (v) rr->v = *v; if (id_ < (1 << 20)) atomic_store_explicit(&g_tser[id_], t ? t->serial : 0, memory_order_release); } - pthread_mutex_unlock(&g_rmu); + os_unfair_lock_unlock(&g_rmu); { // DM_NVN_TREG_ID=n logs every registration of texture ID n: what it holds from then on, and when (the frame being recorded) static int want = -2; if (want == -2) { const char *e = getenv("DM_NVN_TREG_ID"); want = e ? atoi(e) : -1; } if (want == tid) MLOG("TREG id %d frame %llu %s ser=%llu %ux%u format=%#x target %d view %d", tid, (unsigned long long)atomic_load(&g_cur_frame), same ? "(the same)" : "→", @@ -3454,13 +3482,13 @@ void mtl_reg_tex(int tid, const tex_t *t, const view_t *v) { } void mtl_reg_samp(int sid, const samp_par *sp) { int id_ = sid; if (id_ < 0) return; - pthread_mutex_lock(&g_rmu); + os_unfair_lock_lock(&g_rmu); if (id_ >= g_nsreg) { int n = id_ + 256; g_sreg = realloc(g_sreg, n * sizeof *g_sreg); g_sreg_ok = realloc(g_sreg_ok, n); g_sview = realloc(g_sview, n * sizeof *g_sview); g_srid = realloc(g_srid, n * sizeof *g_srid); memset(g_sreg_ok + g_nsreg, 0, n - g_nsreg); memset(g_sview + g_nsreg, 0, (n - g_nsreg) * sizeof *g_sview); memset(g_srid + g_nsreg, 0, (n - g_nsreg) * sizeof *g_srid); g_nsreg = n; } if (g_sview[id_]) { CFRelease(g_sview[id_]); g_sview[id_] = NULL; g_srid[id_] = 0; } g_reg_gen++; g_sreg[id_] = *sp; g_sreg_ok[id_] = 1; - pthread_mutex_unlock(&g_rmu); + os_unfair_lock_unlock(&g_rmu); } // Handles of deferred binds (BindTexturesDeferred) are read at execution: the record keeps the address of the handles (see hle_nvn.c); every reader of a bind record refreshes it first. Command memory is writable, so it is updated in place static inline void bh_fresh(const p_bind_handles *b) { @@ -3667,13 +3695,13 @@ static int mtl_pf_dpf(MTLPixelFormat pf) { // shader's class; a texture made without the format-view usage is upgraded before the next draw pass (img_fit_upgrade), three textures in that game. Caller holds g_rmu static int g_imfit_pend[16]; static void img_fit_upgrade(void) { - pthread_mutex_lock(&g_rmu); + os_unfair_lock_lock(&g_rmu); for (int i = 0; i < g_imfit_npend; i++) { int tid = g_imfit_pend[i]; if (tid <= 0 || tid >= g_ntreg || !g_treg[tid].ok) continue; id<MTLTexture> b = tex_mtl(&g_treg[tid].t); if (b.parentTexture) b = b.parentTexture; if (b && !(b.usage & MTLTextureUsagePixelFormatView)) tex_upgrade_pfv(&g_treg[tid].t, b, MTLPixelFormatR32Uint); } g_imfit_npend = 0; - pthread_mutex_unlock(&g_rmu); + os_unfair_lock_unlock(&g_rmu); } // The same view, in the texture's own class, for a swizzled view (the channel mapping of tex_by_id): Metal gives a swizzled view only the read usage (a base of usage 0x7, a view of 0x1), // so an RGBX8 or RGB565 image was bound as the dummy and its writes were lost. An image takes no channel mapping, as a Vulkan storage image view must have the identity swizzle @@ -3742,17 +3770,17 @@ static inline __attribute__((always_inline)) void *samp_fast(int sid) { } // ---------- Argument buffer blocks: one block per submission, reclaimed after the GPU completes ---------- -@interface NvArena : NSObject { @public id<MTLBuffer> buf; uint64_t gpu, fence; size_t used; } @end +@interface NvArena : NSObject { @public id<MTLBuffer> buf; uint64_t gpu, fence; size_t used, len; } @end // len: buf.length, kept so an allocation sends no message @implementation NvArena @end static NSMutableArray<NvArena *> *g_arenas; static NvArena *g_cur_arena; static NSMutableArray<NvArena *> *g_sub_arenas; static NvArena *arena_new(size_t minimum) { // The for-in loop variable does not retain the object: take a strong reference first and then remove it from the array, otherwise what is returned after the array releases it is a dangling pointer NvArena *r = nil; // the smallest one that fits, so the usual small requests do not keep a large scratch arena alive (ARENA_IDLE, arena_begin) - for (NvArena *it in g_arenas) if (it->fence && it->fence <= g_done && it->buf.length >= minimum && (!r || it->buf.length < r->buf.length)) r = it; + for (NvArena *it in g_arenas) if (it->fence && it->fence <= g_done && it->len >= minimum && (!r || it->len < r->len)) r = it; if (r) { [g_arenas removeObject:r]; r->fence = 0; r->used = 0; return r; } NvArena *a = [NvArena new]; a->buf = [g_dev newBufferWithLength:MAX(minimum, 4u << 20) options:MTLResourceStorageModeShared]; if (!a->buf) MDIE("cannot allocate %zu bytes of frame scratch", minimum); - a->gpu = a->buf.gpuAddress; res_add(a->buf); g_arena_made++; return a; + a->gpu = a->buf.gpuAddress; a->len = a->buf.length; res_add(a->buf); g_arena_made++; return a; } // Arenas above ARENA_KEEP bytes are scratch for large one-off work (a geometry draw's capture, up to 256 MB each). They are kept for reuse while that work goes on and released once // none has been used for ARENA_IDLE submissions: OCTOPATH TRAVELER II draws a 129,032-triangle mesh through a geometry shader now and then in its battles (nvn_geometry.inc geo_split, @@ -3764,12 +3792,14 @@ void arena_begin(void) { g_cur_arena = nil; uint64_t done = g_done; // read as the submission start does (its allocator and visibility ring) for (NSUInteger i = g_arenas.count; i-- > 0;) { NvArena *a = g_arenas[i]; - if (a->fence && a->fence <= done && a->buf.length > ARENA_KEEP && g_fence > a->fence + ARENA_IDLE) { res_remove(a->buf); [g_arenas removeObjectAtIndex:i]; } } + if (a->fence && a->fence <= done && a->len > ARENA_KEEP && g_fence > a->fence + ARENA_IDLE) { res_remove(a->buf); [g_arenas removeObjectAtIndex:i]; } } } void arena_commit(uint64_t fence) { for (NvArena *a in g_sub_arenas) { a->fence = fence; [g_arenas addObject:a]; } [g_sub_arenas removeAllObjects]; g_cur_arena = nil; } -static size_t arena_alloc(size_t n, id<MTLBuffer> *buf) { +// An unqualified id * out-parameter is __autoreleasing under ARC; __strong avoids autoreleasing the arena buffer at every allocation. +// Measurements: docs/reference/frame-rate.md, Arena buffer ownership on allocation. +static size_t arena_alloc(size_t n, __strong id<MTLBuffer> *buf) { n = (n + 255) & ~(size_t)255; - if (!g_cur_arena || g_cur_arena->used + n > g_cur_arena->buf.length) { + if (!g_cur_arena || g_cur_arena->used + n > g_cur_arena->len) { pthread_mutex_lock(&g_mu); g_cur_arena = arena_new(n); pthread_mutex_unlock(&g_mu); [g_sub_arenas addObject:g_cur_arena]; } @@ -4959,7 +4989,7 @@ static int encode_args_(id<MTLCommandEncoder> en, NvShader *sh, int nvn_stage, i static uint64_t bl_pass[6]; static int bl_all = -1; if (bl_all < 0) bl_all = getenv("DM_NVN_PASSDUMP_ALL") != NULL; // Record on every new encode (otherwise only the first time per pass per stage) int bl = passdump() && (bl_all || g_kd_force || bl_pass[nvn_stage] != g_stat_pass); if (bl) { bl_pass[nvn_stage] = g_stat_pass; PD("K %llu st%d key=%s nb=%d", g_stat_pass, nvn_stage, sh->key[0] ? sh->key : sh->builtin ? sh->builtin : "?", sh->rf.nb); } - pthread_mutex_lock(&g_rmu); // The texture / sampler registration tables may be being grown or rewritten by the game thread + os_unfair_lock_lock(&g_rmu); // The texture / sampler registration tables may be being grown or rewritten by the game thread __unsafe_unretained id<MTLTexture> atomic_img[32] = {nil}; uint64_t atomic_rid[32] = {0}; int atomic_real[32] = {0}; const nvn_rbind *atomic_desc[32] = {0}; @@ -5215,7 +5245,7 @@ static int encode_args_(id<MTLCommandEncoder> en, NvShader *sh, int nvn_stage, i } else RED_ONCE(46, "binding kind %d not implemented", b->kind); } #undef USE - pthread_mutex_unlock(&g_rmu); + os_unfair_lock_unlock(&g_rmu); id<MTLBuffer> ab; size_t off = arena_alloc(arg_len, &ab); memcpy((uint8_t *)ab.contents + off, slots, arg_len); bind_args(en, nvn_stage, metal_vertex, ab, g_cur_arena->gpu + off, off, sh->rf.abi); @@ -5466,8 +5496,8 @@ static int state_op(uint32_t op, const void *a) { } case OP_BIND_VATTR: { const p_bind_vattr *b = a; if (S.nattr != b->n || memcmp(S.attr, b->st, 4 * b->n)) { memcpy(S.attr, b->st, 4 * b->n); S.nattr = b->n; g_pso_dirty = 1; } break; } case OP_BIND_VSTREAM: { const p_bind_vstream *b = a; if (S.nstream != b->n || memcmp(S.stream, b->st, 8 * b->n)) { memcpy(S.stream, b->st, 8 * b->n); S.nstream = b->n; g_pso_dirty = 1; } break; } - case OP_BIND_VBUF: { const p_bind_vbuf *b = a; memcpy(&S.vbuf[b->first], b->r, sizeof *b->r * b->n); break; } - case OP_BIND_UBO: { const p_bind_buf *b = a; if (b->stage < 6 && b->index < 16 && memcmp(&S.ubo[b->stage][b->index], b, sizeof *b)) { S.ubo[b->stage][b->index] = *b; g_args_dirty[b->stage] = 1; } break; } + case OP_BIND_VBUF: case OP_BIND_VBUF1: { p_bind_vbuf tmp; const p_bind_vbuf *b = vbuf_rec(op, a, &tmp); memcpy(&S.vbuf[b->first], b->r, sizeof *b->r * b->n); break; } + case OP_BIND_UBO: case OP_BIND_UBO_SMALL: { p_bind_buf tmp; const p_bind_buf *b = bind_buf_rec(op, a, &tmp); if (b->stage < 6 && b->index < 16 && memcmp(&S.ubo[b->stage][b->index], b, sizeof *b)) { S.ubo[b->stage][b->index] = *b; g_args_dirty[b->stage] = 1; } break; } case OP_BIND_SSBO: { const p_bind_buf *b = a; if (b->stage < 6 && b->index < 16 && memcmp(&S.ssbo[b->stage][b->index], b, sizeof *b)) { S.ssbo[b->stage][b->index] = *b; g_args_dirty[b->stage] = 1; } break; } case OP_BIND_TEX: case OP_BIND_TEX1: { p_bind_handles t1; const p_bind_handles *b = bind_rec(op, a, &t1); if (b->n < 0 || b->n > 32) { // the recorder never writes more than 32 (bind_handles): the record was overwritten after it was recorded @@ -5680,7 +5710,7 @@ static int draw_samples_rt(NvProg *pr) { // Sampled attachments keep the origi for (int i = 0; i < sh->rf.nb; i++) { const nvn_rbind *b = &sh->rf.b[i]; uint64_t h; if (b->kind == NVN_RB_IMG) h = b->reg < 32 && sh->stage < 6 ? S.img[sh->stage][b->reg] : 0; else if (b->kind == NVN_RB_TEX || b->kind == NVN_RB_TBUF) h = sh->stage < 6 ? tex_slot_h(S.tex[sh->stage], S.img[sh->stage], b->reg) : 0; else continue; int tid = (int)(h & 0xfffff); if (!tid) continue; - pthread_mutex_lock(&g_rmu); uint64_t ser = tid < g_ntreg && g_treg[tid].ok ? g_treg[tid].t.serial : 0; pthread_mutex_unlock(&g_rmu); + os_unfair_lock_lock(&g_rmu); uint64_t ser = tid < g_ntreg && g_treg[tid].ok ? g_treg[tid].t.serial : 0; os_unfair_lock_unlock(&g_rmu); for (int j = 0; j < nr; j++) if (ser && ser == rs[j]) return 1; } } return 0; } @@ -6709,11 +6739,11 @@ static void fb_prepare(NvProg *pr) { NvShader *fs = pr->sh[1]; for (int i = 0; i < fs->rf.nb && g_fb_n < 4; i++) { const nvn_rbind *b = &fs->rf.b[i]; if (b->kind != NVN_RB_TEX || b->reg >= 128) continue; int tid = (int)(tex_slot_h(S.tex[1], S.img[1], b->reg) & 0xfffff); if (!tid) continue; - pthread_mutex_lock(&g_rmu); uint64_t ser = tid < g_ntreg && g_treg[tid].ok ? g_treg[tid].t.serial : 0; pthread_mutex_unlock(&g_rmu); + os_unfair_lock_lock(&g_rmu); uint64_t ser = tid < g_ntreg && g_treg[tid].ok ? g_treg[tid].t.serial : 0; os_unfair_lock_unlock(&g_rmu); if (!ser) continue; for (int k = 0; k < R.rt.n && k < 8 && g_fb_n < 4; k++) { if (!R.rt.color[k] || R.rt.color[k]->serial != ser) continue; uint64_t aser; int lv, sl; rt_sub(&R.rt, k, &aser, &lv, &sl); - pthread_mutex_lock(&g_rmu); void *tv = NULL; tex_fast_rid(tid, &tv); id<MTLTexture> t = (__bridge id<MTLTexture>)tv; pthread_mutex_unlock(&g_rmu); + os_unfair_lock_lock(&g_rmu); void *tv = NULL; tex_fast_rid(tid, &tv); id<MTLTexture> t = (__bridge id<MTLTexture>)tv; os_unfair_lock_unlock(&g_rmu); if (!t) continue; int dup = 0; for (int q = 0; q < g_fb_n; q++) dup |= g_fb_src[q] == t; if (dup) continue; id<MTLTexture> base = t.parentTexture ? t.parentTexture : t; NSUInteger tl = t.parentTexture ? t.parentRelativeLevel : 0, ts = t.parentTexture ? t.parentRelativeSlice : 0; @@ -7185,8 +7215,8 @@ static void lc_take(NvProg *pr) { static int lc_keeps(uint32_t op) { switch (op) { case OP_SET_RT: case OP_CLEAR_COLOR: case OP_CLEAR_DS: case OP_DRAW: case OP_DRAW_IDX: case OP_BEGIN: case OP_JUMP: case OP_CALL: - case OP_SCISSOR: case OP_BIND_PROGRAM: case OP_BIND_STATE: case OP_BIND_VATTR: case OP_BIND_VSTREAM: case OP_BIND_VBUF: - case OP_BIND_UBO: case OP_BIND_SSBO: case OP_BIND_TEX: case OP_BIND_TEX1: case OP_BIND_SAMP: case OP_BIND_IMG: case OP_STENCIL: + case OP_SCISSOR: case OP_BIND_PROGRAM: case OP_BIND_STATE: case OP_BIND_VATTR: case OP_BIND_VSTREAM: case OP_BIND_VBUF: case OP_BIND_VBUF1: + case OP_BIND_UBO: case OP_BIND_UBO_SMALL: case OP_BIND_SSBO: case OP_BIND_TEX: case OP_BIND_TEX1: case OP_BIND_SAMP: case OP_BIND_IMG: case OP_STENCIL: case OP_POLY_OFFSET: case OP_VIEWPORTS: case OP_DEPTH_RANGE: case OP_DEPTH_CLAMP: case OP_VP_SWIZZLE: case OP_BLEND_COLOR: case OP_COPY_STRIDE: case OP_TF_BIND: case OP_TF_STATE: case OP_RAST_DISCARD: case OP_PRIM_RESTART: case OP_BARRIER: case OP_SET_TEX_POOL: case OP_SET_SAMP_POOL: case OP_RENDER_COND: case OP_UPDATE_UB: case OP_COPY_B2B: case OP_CLEAR_BUF: return 1; @@ -7621,6 +7651,9 @@ void mtl_read_texels(const tex_t *t, unsigned level, unsigned layer0, const NVNc free(lin); return; } + // A guest thread can live indefinitely without draining its outer pool: release readback temporaries here, after the GPU and CPU copies finish. + // Finding: docs/reference/memory.md, GPU readback without an autorelease pool. + @autoreleasepool { id<MTLTexture> mt = tex_mtl(t); if (!mt) { memset(dst, 0, img * nz); return; } mtl_finish_gpu_at(WS_READTEX); @@ -7631,6 +7664,7 @@ void mtl_read_texels(const tex_t *t, unsigned level, unsigned layer0, const NVNc sourceSize:MTLSizeMake(r->width, r->height, 1) toBuffer:buf destinationOffset:z * wb * hb destinationBytesPerRow:wb destinationBytesPerImage:wb * hb]; enc_b_end(b); [cb commit]; [cb waitUntilCompleted]; for (int z = 0; z < nz; z++) for (size_t y = 0; y < hb; y++) memcpy(dst + z * img + y * row, (uint8_t *)buf.contents + z * wb * hb + y * wb, wb); + } } static void copy_b2t(const p_copy_b2t *c) { if (c->tex) zskip_del(c->tex->serial); @@ -8210,8 +8244,8 @@ void mtl_exec(const uint8_t *p) { case OP_DRAW: { if (g_rc_skip) break; p_draw t; g_exec_cur = p; g_exec_next = p + h.size; draw(draw_rec(a, h.size, &t), NULL); g_exec_next = NULL; break; } // Lookahead start point case OP_DRAW_IDX: { if (g_rc_skip) break; p_draw_idx t; g_exec_cur = p; g_exec_next = p + h.size; draw(NULL, draw_idx_rec(a, h.size, &t)); g_exec_next = NULL; break; } // Do not mark dirty if the state did not change (the game often re-sends the same program / state block / vertex format before each draw; the dirty flag only decides whether to re-fetch the pipeline / depth-stencil object, and equal values mean the same object) - case OP_SCISSOR: case OP_BIND_PROGRAM: case OP_BIND_STATE: case OP_BIND_VATTR: case OP_BIND_VSTREAM: case OP_BIND_VBUF: - case OP_BIND_UBO: case OP_BIND_SSBO: case OP_BIND_TEX: case OP_BIND_TEX1: case OP_BIND_SAMP: case OP_BIND_IMG: case OP_STENCIL: + case OP_SCISSOR: case OP_BIND_PROGRAM: case OP_BIND_STATE: case OP_BIND_VATTR: case OP_BIND_VSTREAM: case OP_BIND_VBUF: case OP_BIND_VBUF1: + case OP_BIND_UBO: case OP_BIND_UBO_SMALL: case OP_BIND_SSBO: case OP_BIND_TEX: case OP_BIND_TEX1: case OP_BIND_SAMP: case OP_BIND_IMG: case OP_STENCIL: case OP_POLY_OFFSET: case OP_VIEWPORTS: case OP_DEPTH_RANGE: case OP_DEPTH_CLAMP: case OP_VP_SWIZZLE: case OP_BLEND_COLOR: case OP_COPY_STRIDE: state_op(h.op, a); break; case OP_TF_BIND: { const p_tf_bind *b = a; PD("Y tf bind %d addr=%#llx size=%llu", b->index, (unsigned long long)b->addr, (unsigned long long)b->size); state_op(h.op, a); break; } @@ -8257,11 +8291,11 @@ void mtl_res_snapshot(mtl_res_count *out) { out->pools = (uint32_t)g_npool; out->pool_adds = g_pool_adds; pthread_mutex_unlock(&g_mu); - pthread_mutex_lock(&g_rmu); + os_unfair_lock_lock(&g_rmu); out->view_slots = (uint32_t)g_ntreg; for (int i = 0; i < g_ntreg; i++) if (g_treg[i].view) out->view_nonnull++; for (int i = 0; i < g_nsreg; i++) if (g_sview[i]) out->sview_nonnull++; - pthread_mutex_unlock(&g_rmu); + os_unfair_lock_unlock(&g_rmu); } // Resource counts (read by long-run stability L3): the host calls it per game tick / event (every 250 ticks, loading screen dismissed, exit), not sampled by wall clock / present count. // Line format: [nvn] resource counts tick <tick number> <trigger>: <name> <value> ... (names contain no spaces, single-space separated); new fields are appended to the end of this line @@ -8401,11 +8435,11 @@ void dm_gfx_texraw(uint64_t serial, const char *path) { } // The Mac's DM_CTL "texlist <NVN target>" (hle_hid.c): the registered textures of that target (2 = 3D), with their storage (guest address, size) and block-linear layout, for "memdump" void dm_gfx_texlist(int target) { - pthread_mutex_lock(&g_rmu); + os_unfair_lock_lock(&g_rmu); for (int i = 1; i < g_ntreg; i++) { const tex_t *t = &g_treg[i].t; if (!g_treg[i].ok || t->d.target != target) continue; MLOG("[texlist] tid=%d ser=%llu target %d %ux%ux%u format %#x levels %u flags %#x storage %p size %#llx block height/depth log2 %u/%u", i, (unsigned long long)t->serial, t->d.target, t->d.width, t->d.height, t->d.depth, t->d.format, t->d.levels, t->d.flags, (void *)t->mem, (unsigned long long)t->L.size, t->L.tileH, t->L.tileD); } - pthread_mutex_unlock(&g_rmu); + os_unfair_lock_unlock(&g_rmu); } void dm_gfx_texdump_next(const char *path) { if (g_td_frame == -2) td_note(nil, 0); // read DM_NVN_TEXDUMP first, so it does not override the request diff --git a/dm/gfx/nvn/nvn_obj.h b/dm/gfx/nvn/nvn_obj.h index e2af98a..922dbc9 100644 --- a/dm/gfx/nvn/nvn_obj.h +++ b/dm/gfx/nvn/nvn_obj.h @@ -103,7 +103,7 @@ enum { OP_BIND_PROGRAM, OP_BIND_STATE, OP_BIND_VATTR, OP_BIND_VSTREAM, OP_BIND_VBUF, OP_BIND_UBO, OP_BIND_SSBO, OP_BIND_TEX, OP_BIND_SAMP, OP_STENCIL, OP_POLY_OFFSET, OP_DRAW, OP_DRAW_IDX, OP_DISPATCH, OP_BARRIER, - OP_SET_TEX_POOL, OP_SET_SAMP_POOL, OP_COPY_B2T, OP_COPY_STRIDE, OP_BLEND_COLOR, OP_COPY_T2B, OP_BIND_IMG, OP_COPY_T2T, OP_RESET_COUNTER, OP_CLEAR_TEX, OP_CLEAR_BUF, OP_DEPTH_CLAMP, OP_RENDER_COND, OP_FENCE_SYNC, OP_TF_BIND, OP_TF_STATE, OP_RAST_DISCARD, OP_VP_SWIZZLE, OP_BIND_TEX1, OP_PRIM_RESTART, OP_COUNT_ + OP_SET_TEX_POOL, OP_SET_SAMP_POOL, OP_COPY_B2T, OP_COPY_STRIDE, OP_BLEND_COLOR, OP_COPY_T2B, OP_BIND_IMG, OP_COPY_T2T, OP_RESET_COUNTER, OP_CLEAR_TEX, OP_CLEAR_BUF, OP_DEPTH_CLAMP, OP_RENDER_COND, OP_FENCE_SYNC, OP_TF_BIND, OP_TF_STATE, OP_RAST_DISCARD, OP_VP_SWIZZLE, OP_BIND_TEX1, OP_PRIM_RESTART, OP_BIND_UBO_SMALL, OP_BIND_VBUF1, OP_COUNT_ }; // Payload (immediately follows op_hdr) typedef struct { int32_t n; const tex_t *color[8]; view_t cview[8]; uint8_t has_cview[8]; const tex_t *depth; view_t dview; uint8_t has_dview; } p_set_rt; @@ -121,7 +121,22 @@ typedef struct { int32_t kind; uint8_t bytes[0x18]; } p_bind_state; // kind typedef struct { int32_t first, n; uint32_t st[16]; } p_bind_vattr; typedef struct { int32_t first, n; vs_st st[16]; } p_bind_vstream; typedef struct { int32_t first, n; NVNbufferRange r[16]; } p_bind_vbuf; +// Games measure scratch recordings to size resource heaps. Keep a single bounded binding at 24 bytes including its header (the native vertex-buffer command is 24 bytes). +// Larger ranges retain the full record; replay expands compact records to the same state, with the full 64-bit GPU address. +typedef struct { uint32_t index, size; uint64_t addr; } p_bind_vbuf1; +static inline const p_bind_vbuf *vbuf_rec(uint32_t op, const void *a, p_bind_vbuf *t) { + if (op != OP_BIND_VBUF1) return a; + const p_bind_vbuf1 *b = a; + t->first = (int32_t)b->index; t->n = 1; t->r[0] = (NVNbufferRange){b->addr, b->size}; return t; +} typedef struct { int32_t stage, index; uint64_t addr, size; } p_bind_buf; // UBO / SSBO +// Uniform bindings likewise take 24 bytes instead of 32 (the native inline command takes 20). See memory.md, model heaps sized by command records. +typedef struct { uint16_t stage, index; uint32_t size; uint64_t addr; } p_bind_ubo_small; +static inline const p_bind_buf *bind_buf_rec(uint32_t op, const void *a, p_bind_buf *t) { + if (op != OP_BIND_UBO_SMALL) return a; + const p_bind_ubo_small *b = a; + *t = (p_bind_buf){b->stage, b->index, b->addr, b->size}; return t; +} // defer / src: the handles of BindTexturesDeferred are read from src at execution time (h at recording time is only a fallback); the replay layer refreshes them with bh_fresh before reading the record (nvn_mtl.m) typedef struct { int32_t stage, first, n, defer; uint64_t src; uint64_t h[32]; } p_bind_handles; // Separate texture / sampler handles // nvnCommandBufferBindTexture (one combined handle) is laid out as the hardware's command: the handle 16 bytes from the start of the record, where an engine that patches its recorded commands diff --git a/dm/gfx/tests/.gitignore b/dm/gfx/tests/.gitignore index 873505d..c248cf9 100644 --- a/dm/gfx/tests/.gitignore +++ b/dm/gfx/tests/.gitignore @@ -58,6 +58,8 @@ t_tbuf2d t_tbuf2d.dSYM/ t_view_levels t_view_levels.dSYM/ +t_upload_budget +t_upload_budget.dSYM/ t_view_swizzle t_view_swizzle.dSYM/ t_vk_desc diff --git a/dm/gfx/tests/Makefile b/dm/gfx/tests/Makefile index 351b0a1..3f16d5f 100644 --- a/dm/gfx/tests/Makefile +++ b/dm/gfx/tests/Makefile @@ -115,6 +115,13 @@ t_view_swizzle: t_view_swizzle.m $(MTL_T_DEP) clang $(CFLAGS) -fobjc-arc -o $@ t_view_swizzle.m $(MTL_T_SRC) $(FW) t_view_levels: t_view_levels.m $(MTL_T_DEP) clang $(CFLAGS) -fobjc-arc -o $@ t_view_levels.m $(MTL_T_SRC) $(FW) +t_upload_budget: t_upload_budget.m $(MTL_T_DEP) + clang $(CFLAGS) -fobjc-arc -o $@ t_upload_budget.m $(MTL_T_SRC) $(FW) +check_t_upload_budget: t_upload_budget + MTL_DEBUG_LAYER=1 ./t_upload_budget + DM_NVN_ENC_THREAD=1 MTL_DEBUG_LAYER=1 ./t_upload_budget +run: check_t_upload_budget +.PHONY: check_t_upload_budget t_linear_readback: t_linear_readback.m $(MTL_T_DEP) clang $(CFLAGS) -fobjc-arc -o $@ t_linear_readback.m ../../host/crash.c ../../host/hle_bigstack.c t_host_stubs.c $(filter-out ../nvn/nvn_mtl.m,$(GFX)) $(FW) .PHONY: check_t_linear_readback diff --git a/dm/gfx/tests/t_nvn.c b/dm/gfx/tests/t_nvn.c index 8834a6c..27b489b 100644 --- a/dm/gfx/tests/t_nvn.c +++ b/dm/gfx/tests/t_nvn.c @@ -247,6 +247,96 @@ int main(void) { EXPECT("the bind reads the handle patched at its start + 0x10: slot 3, texture 1235, sampler 511", 1, rh.op == OP_BIND_TEX1 && bh->n == 1 && bh->first == 3 && bh->stage == NVN_SHADER_STAGE_FRAGMENT && bh->h[0] == patched); P(nvnCommandBufferFinalize)(&c2); P(nvnMemoryPoolFinalize)(&mp); } + // Uniform bindings are measured into resource budgets too. A bounded binding needs + // 8 header bytes plus 16 payload bytes; keep the full record for larger ranges. + { + static uint8_t m[4096] __attribute__((aligned(4096))); NVNmemoryPoolBuilder mb; NVNmemoryPool mp; NVNcommandBuffer c2; + P(nvnMemoryPoolBuilderSetDefaults)(&mb); P(nvnMemoryPoolBuilderSetStorage)(&mb, m, sizeof m); P(nvnMemoryPoolInitialize)(&mp, &mb); + P(nvnCommandBufferInitialize)(&c2, &dev); + P(nvnCommandBufferAddCommandMemory)(&c2, &mp, 0, sizeof m); P(nvnCommandBufferBeginRecording)(&c2); + P(nvnCommandBufferBindUniformBuffer)(&c2, NVN_SHADER_STAGE_FRAGMENT, 13, UINT64_C(0x123456789abc0000), 65536); + NVNcommandHandle first = P(nvnCommandBufferEndRecording)(&c2); + size_t first_size = P(nvnCommandBufferGetCommandMemoryUsed)(&c2); + EXPECT("maximum uniform range: 24-byte bind + END", 32, first_size); + op_hdr rh; memcpy(&rh, m, sizeof rh); + EXPECT("bounded uniform record is 24 bytes", 24, rh.size); + p_bind_buf decoded; const p_bind_buf *b = bind_buf_rec(rh.op, m + 8, &decoded); + EXPECT("bounded uniform decode keeps stage, index, address, size", 1, + b->stage == NVN_SHADER_STAGE_FRAGMENT && b->index == 13 && b->addr == UINT64_C(0x123456789abc0000) && b->size == 65536); + op_hdr end; memcpy(&end, m + rh.size, sizeof end); + EXPECT("uniform bind preserves its adjacent END", 1, end.op == OP_END && end.size == 8 && (const uint8_t *)first == m); + uint8_t saved[64]; memcpy(saved, m, first_size); + P(nvnCommandBufferAddCommandMemory)(&c2, &mp, (ptrdiff_t)first_size, sizeof m - first_size); P(nvnCommandBufferBeginRecording)(&c2); + NVNbufferRange ranges[] = {{UINT64_C(0xfedcba9876540000), 256}, {UINT64_C(0x1122334455660000), 0}}; + P(nvnCommandBufferBindUniformBuffers)(&c2, NVN_SHADER_STAGE_VERTEX, 2, 2, ranges); + P(nvnCommandBufferEndRecording)(&c2); + memset(ranges, 0, sizeof ranges); + EXPECT("plural uniform binds: 2 x 24 + END", 56, P(nvnCommandBufferGetCommandMemoryUsed)(&c2)); + EXPECT("independent uniform recording preserves predecessor", 0, memcmp(saved, m, first_size)); + size_t pos = first_size; + memcpy(&rh, m + pos, sizeof rh); b = bind_buf_rec(rh.op, m + pos + 8, &decoded); + EXPECT("plural first uniform keeps copied range and slot", 1, + b->stage == NVN_SHADER_STAGE_VERTEX && b->index == 2 && b->addr == UINT64_C(0xfedcba9876540000) && b->size == 256); + pos += rh.size; + memcpy(&rh, m + pos, sizeof rh); b = bind_buf_rec(rh.op, m + pos + 8, &decoded); + EXPECT("plural second uniform preserves zero range and next slot", 1, + b->stage == NVN_SHADER_STAGE_VERTEX && b->index == 3 && b->addr == UINT64_C(0x1122334455660000) && b->size == 0); + memcpy(&end, m + pos + rh.size, sizeof end); + EXPECT("plural uniform binds preserve their END", 1, end.op == OP_END && end.size == 8); + P(nvnCommandBufferAddCommandMemory)(&c2, &mp, 512, sizeof m - 512); P(nvnCommandBufferBeginRecording)(&c2); + P(nvnCommandBufferBindUniformBuffer)(&c2, NVN_SHADER_STAGE_COMPUTE, 3, UINT64_C(0xffff123456780000), UINT64_C(0x100000001)); + P(nvnCommandBufferEndRecording)(&c2); + memcpy(&rh, m + 512, sizeof rh); + EXPECT("wide uniform range keeps full record + END", 40, P(nvnCommandBufferGetCommandMemoryUsed)(&c2)); + EXPECT("wide uniform range keeps full opcode and size", 1, rh.op == OP_BIND_UBO && rh.size == 32); + b = bind_buf_rec(rh.op, m + 520, &decoded); + EXPECT("wide uniform decode preserves all 64 bits", 1, + b->stage == NVN_SHADER_STAGE_COMPUTE && b->index == 3 && b->addr == UINT64_C(0xffff123456780000) && b->size == UINT64_C(0x100000001)); + P(nvnCommandBufferFinalize)(&c2); P(nvnMemoryPoolFinalize)(&mp); + } + // One vertex range needs 8 header bytes plus 16 payload bytes; arrays and + // ranges wider than 32 bits retain the general representation. + { + static uint8_t m[4096] __attribute__((aligned(4096))); NVNmemoryPoolBuilder mb; NVNmemoryPool mp; NVNcommandBuffer c2; + P(nvnMemoryPoolBuilderSetDefaults)(&mb); P(nvnMemoryPoolBuilderSetStorage)(&mb, m, sizeof m); P(nvnMemoryPoolInitialize)(&mp, &mb); + P(nvnCommandBufferInitialize)(&c2, &dev); + P(nvnCommandBufferAddCommandMemory)(&c2, &mp, 0, sizeof m); P(nvnCommandBufferBeginRecording)(&c2); + P(nvnCommandBufferBindVertexBuffer)(&c2, 15, UINT64_C(0xfedcba9876540000), UINT32_MAX); + P(nvnCommandBufferEndRecording)(&c2); + EXPECT("single vertex buffer: 24-byte bind + END", 32, P(nvnCommandBufferGetCommandMemoryUsed)(&c2)); + op_hdr rh, end; memcpy(&rh, m, sizeof rh); memcpy(&end, m + rh.size, sizeof end); + EXPECT("single vertex binding record is 24 bytes", 24, rh.size); + EXPECT("single vertex binding preserves adjacent END", 1, end.op == OP_END && end.size == 8); + p_bind_vbuf tmp; const p_bind_vbuf *b = vbuf_rec(rh.op, m + 8, &tmp); + EXPECT("single vertex decode preserves slot, full address, maximum size", 1, + b->first == 15 && b->n == 1 && b->r[0].address == UINT64_C(0xfedcba9876540000) && b->r[0].size == UINT32_MAX); + NVNbufferRange ranges[] = {{UINT64_C(0x1122334455660000), 0}, {UINT64_C(0x9988776655440000), 256}}; + P(nvnCommandBufferAddCommandMemory)(&c2, &mp, 128, sizeof m - 128); P(nvnCommandBufferBeginRecording)(&c2); + P(nvnCommandBufferBindVertexBuffers)(&c2, 3, 1, ranges); P(nvnCommandBufferEndRecording)(&c2); + EXPECT("one-element vertex array: 24-byte bind + END", 32, P(nvnCommandBufferGetCommandMemoryUsed)(&c2)); + memcpy(&rh, m + 128, sizeof rh); b = vbuf_rec(rh.op, m + 136, &tmp); + EXPECT("one-element vertex array decode preserves zero range", 1, + b->first == 3 && b->n == 1 && b->r[0].address == UINT64_C(0x1122334455660000) && b->r[0].size == 0); + P(nvnCommandBufferAddCommandMemory)(&c2, &mp, 256, sizeof m - 256); P(nvnCommandBufferBeginRecording)(&c2); + P(nvnCommandBufferBindVertexBuffers)(&c2, 4, 2, ranges); P(nvnCommandBufferEndRecording)(&c2); + memset(ranges, 0, sizeof ranges); + EXPECT("two vertex ranges retain array record + END", 56, P(nvnCommandBufferGetCommandMemoryUsed)(&c2)); + memcpy(&rh, m + 256, sizeof rh); + EXPECT("two vertex ranges retain array opcode", OP_BIND_VBUF, rh.op); + b = vbuf_rec(rh.op, m + 264, &tmp); + EXPECT("vertex array decode preserves both copied ranges", 1, + b->first == 4 && b->n == 2 && b->r[0].address == UINT64_C(0x1122334455660000) && !b->r[0].size && + b->r[1].address == UINT64_C(0x9988776655440000) && b->r[1].size == 256); + P(nvnCommandBufferAddCommandMemory)(&c2, &mp, 384, sizeof m - 384); P(nvnCommandBufferBeginRecording)(&c2); + P(nvnCommandBufferBindVertexBuffer)(&c2, 7, UINT64_C(0xabcd123456780000), UINT64_C(0x100000001)); P(nvnCommandBufferEndRecording)(&c2); + EXPECT("wide vertex range retains full record + END", 40, P(nvnCommandBufferGetCommandMemoryUsed)(&c2)); + memcpy(&rh, m + 384, sizeof rh); + EXPECT("wide vertex range retains array opcode", OP_BIND_VBUF, rh.op); + b = vbuf_rec(rh.op, m + 392, &tmp); + EXPECT("wide vertex range decode preserves all 64 bits", 1, + b->first == 7 && b->n == 1 && b->r[0].address == UINT64_C(0xabcd123456780000) && b->r[0].size == UINT64_C(0x100000001)); + P(nvnCommandBufferFinalize)(&c2); P(nvnMemoryPoolFinalize)(&mp); + } // 5b'. Fixed slots sized by GetCommandMemoryUsed, as Tears of the Kingdom gives each model's prerecorded lists (GitHub issue #61): a list measured at S bytes is recorded // into slot k = base + S × k with AddCommandMemory(max(S, 512)), and slot k + 1 is recorded right after it. The list here is an uniform update of n bytes and a // uniform buffer bind (the last record before the END in the crash), the next slot's starts with a CALL. Walking slot 0 afterwards must reach its own END through @@ -275,7 +365,13 @@ int main(void) { if (h.op == OP_JUMP) { const uint8_t *t; memcpy(&t, p + 8, 8); int in = t >= m && t < m + sizeof m; for (int i = 0; i < g_nhostseg; i++) in |= t >= g_hostseg[i].mem && t < g_hostseg[i].mem + g_hostseg[i].n; if (!in) { ok = 0; break; } jumped = 1; p = t; continue; } - if (k < 4) ops[k] = (int)h.op; k++; + uint32_t op = h.op; + if (op == OP_BIND_UBO || op == OP_BIND_UBO_SMALL) { + p_bind_buf tmp; const p_bind_buf *b = bind_buf_rec(op, p + 8, &tmp); + ok &= b->stage == NVN_SHADER_STAGE_FRAGMENT && b->index == 4 && b->addr == 0x20000 && b->size == 0x100; + op = OP_BIND_UBO; + } + if (k < 4) ops[k] = (int)op; k++; if (h.op == OP_END) break; p += h.size; } @@ -285,7 +381,7 @@ int main(void) { } printf("(first broken slot size %d)\n", first_bad); EXPECT("slotted lists replay whole after the next slot is recorded", 0, broken); - EXPECT("lists of 64..504 bytes stay in their 512 (no host segment)", (504 - 64) / 8 + 1, jumps_fit); + EXPECT("lists of 56..504 bytes stay in their 512 (no host segment)", (504 - 56) / 8 + 1, jumps_fit); P(nvnCommandBufferFinalize)(&c3); P(nvnMemoryPoolFinalize)(&mp); } // 5b''. Host segment sizes (hostseg_size), as Super Mario Odyssey grants 512 bytes from an empty pool before recording: such a grant gets a HOSTSEG_SMALL segment; a recording diff --git a/dm/gfx/tests/t_readback.m b/dm/gfx/tests/t_readback.m index 069fd8e..f47e196 100644 --- a/dm/gfx/tests/t_readback.m +++ b/dm/gfx/tests/t_readback.m @@ -15,6 +15,21 @@ uint64_t now_ns(void) { return bp_now(); } static int cases, failed; #define EXPECT(n,w,v) do { int a=(w), b=(v); cases++; printf("%s expected %d -> got %d %s\n",n,a,b,a==b?"PASS":"FAIL"); failed+=a!=b; } while (0) +// Observe the real readback buffer without retaining it. The caller's outer pool stays open, +// as on a long-lived guest thread; ReadTexels must drain its own temporary Metal objects. +@interface ReadbackDevice : NSObject +@property(nonatomic, strong) id<MTLDevice> device; +@property(nonatomic, weak) id<MTLBuffer> lastBuffer; +@property(nonatomic) NSUInteger allocations; +@end +@implementation ReadbackDevice +- (id<MTLBuffer>)newBufferWithLength:(NSUInteger)length options:(MTLResourceOptions)options { + id<MTLBuffer> buffer = [_device newBufferWithLength:length options:options]; + _lastBuffer = buffer; _allocations++; return buffer; +} +- (id)forwardingTargetForSelector:(SEL)selector { (void)selector; return _device; } +@end + int main(void) { @autoreleasepool { mtl_init(); void *mem = NULL; if (posix_memalign(&mem, getpagesize(), 262144)) abort(); memset(mem, 0, 262144); @@ -41,9 +56,13 @@ int main(void) { @autoreleasepool { for (int y=0; y<4; y++) { for (int x=0; x<8; x++) bad += memcmp(out+y*64+x*4, expected, 4) != 0; for (int x=32; x<64; x++) bad += out[y*64+x] != 0xEE; } EXPECT("pending clear -> GPU copy, literal pixels and untouched row padding", 0, bad); + ReadbackDevice *device = [ReadbackDevice new]; device.device = g_dev; g_dev = (id)device; uint8_t cpu[128]; memset(cpu, 0xEE, sizeof cpu); h_nvnTextureReadTexels(&tex, NULL, ®ion, cpu); bad = 0; + g_dev = device.device; for (int i=0; i<32; i++) bad += memcmp(cpu+i*4, expected, 4) != 0; EXPECT("GPU clear -> CPU ReadTexels, 32 literal pixels", 0, bad); + EXPECT("ReadTexels used the GPU readback path", 1, device.allocations); + EXPECT("ReadTexels releases its readback buffer before the caller's pool drains", 1, device.lastBuffer == nil); h_nvnTextureFinalize(&tex); mtl_pool_del(mem); free(mem); printf("READBACK cases=%d failed=%d\n", cases, failed); return failed != 0; } } diff --git a/dm/gfx/tests/t_reorder_chain.m b/dm/gfx/tests/t_reorder_chain.m index b6fbbd8..65fdd63 100644 --- a/dm/gfx/tests/t_reorder_chain.m +++ b/dm/gfx/tests/t_reorder_chain.m @@ -129,16 +129,22 @@ static uint64_t query_case(int reordered, int mode) { const char *names[]={"query","same_range_query","later_reset_overrides_query"}; save_raw(names[mode],reordered,q,sizeof q); mtl_pool_del(c.p); free(c.p); return q[0]; } -static void uniform_case(int reordered,uint8_t pixels[256]) { +static void uniform_case(int reordered,int compact,uint8_t pixels[256]) { void *mem=NULL; if(posix_memalign(&mem,getpagesize(),getpagesize())) abort(); memset(mem,0,getpagesize()); float red[4]={1,0,0,1},green[4]={0,1,0,1}; memcpy(mem,red,16); mtl_pool_add(mem,getpagesize()); tex_t a=texture(8,1,0),b=texture(8,1,0),out=texture(8,1,0),e=texture(8,1,0); chain c=make_chain(); clear(&c,&out,0,NULL,0,0,0); clear(&c,&e,0,NULL,0,0,0); stencil(&c,0,8,1,255,255,0); blending(&c,0); - p_bind_buf ub={.stage=1,.index=0,.addr=(uint64_t)mem,.size=16}; emit(&c,OP_BIND_UBO,&ub,sizeof ub); + if(compact) { + p_bind_ubo_small ub={.stage=1,.index=0,.addr=(uint64_t)mem,.size=16}; emit(&c,OP_BIND_UBO_SMALL,&ub,sizeof ub); + } else { + p_bind_buf ub={.stage=1,.index=0,.addr=(uint64_t)mem,.size=16}; emit(&c,OP_BIND_UBO,&ub,sizeof ub); + } if(!reordered) { clear(&c,&a,0,NULL,0,0,0); draw_prog(&c,4,0); } struct { uint64_t dst,n; float color[4]; } update={(uint64_t)mem,16,{0}}; memcpy(update.color,green,16); emit(&c,OP_UPDATE_UB,&update,sizeof update); if(reordered) { clear(&c,&a,0,NULL,0,0,0); draw_prog(&c,4,0); } clear(&c,&b,0,NULL,0,0,0); draw_prog(&c,4,0); result r=execute(&c,&a,&b,&out,&e,NULL); save_result("UB_write",reordered,&r); memcpy(pixels,r.a,256); + EXPECT("uniform replay restores canonical stage and slot",1,S.ubo[1][0].stage==1&&S.ubo[1][0].index==0); + EXPECT("uniform replay restores full address and size",1,S.ubo[1][0].addr==(uint64_t)mem&&S.ubo[1][0].size==16); uniform_pixels("second draw after the UB update always green",r.b,0,255,0,255); mtl_pool_del(mem); free(mem); } // A view's accessible range cannot be judged by base alone: the shader reads relative to the second level/second layer, and G writes that subresource. @@ -253,7 +259,11 @@ int main(int argc,char **argv) { @autoreleasepool { } uint64_t q0=query_case(0,0),q1=query_case(1,0); EXPECT("reset original range 4x4 samples passed",16,q0); EXPECT("across-reset negative 4x4+8x8 samples passed",80,q1); EXPECT("query lifetime must not count as independent",all_independent?1:0,q0==q1); - uint8_t u0[256],u1[256]; uniform_case(0,u0); uniform_case(1,u1); uniform_pixels("UB original order first draw red",u0,255,0,0,255); uniform_pixels("across UB write first draw green",u1,0,255,0,255); + uint8_t u0[256],u1[256],uc0[256],uc1[256]; uniform_case(0,0,u0); uniform_case(1,0,u1); + uniform_case(0,1,uc0); uniform_case(1,1,uc1); + uniform_pixels("UB original order first draw red",u0,255,0,0,255); uniform_pixels("across UB write first draw green",u1,0,255,0,255); + EXPECT("compact UB original order matches full record pixels",0,memcmp(u0,uc0,256)); + EXPECT("compact UB reordered matches full record pixels",0,memcmp(u1,uc1,256)); EXPECT("UB write must not move across draws",all_independent?1:0,!memcmp(u0,u1,256)); for(int array=0;array<2;array++) { const char *name=array?"whole_slice_range":"whole_mip_range"; printf("CASE %s\n",name); result o=range_case(array,0),r=range_case(array,1); uniform_pixels("range original order second subresource blue",o.b,0,0,255,255); diff --git a/dm/gfx/tests/t_stencil_view.m b/dm/gfx/tests/t_stencil_view.m index b6cc0f1..ecdd70d 100644 --- a/dm/gfx/tests/t_stencil_view.m +++ b/dm/gfx/tests/t_stencil_view.m @@ -44,7 +44,7 @@ int main(void) { @autoreleasepool { // A stencil-mode view, and the texture built in stencil mode, are X32_Stencil8 views of the depth-stencil storage itself (no upgraded copy) view_t v = {.set_dsmode=1, .dsmode=NVN_TEXTURE_DEPTH_STENCIL_MODE_STENCIL}; mtl_reg_tex(800 + 2 * i, &s, &v); mtl_reg_tex(801 + 2 * i, &sb, NULL); - pthread_mutex_lock(&g_rmu); id<MTLTexture> sv = tex_by_id(800 + 2 * i), bv = tex_by_id(801 + 2 * i); pthread_mutex_unlock(&g_rmu); + os_unfair_lock_lock(&g_rmu); id<MTLTexture> sv = tex_by_id(800 + 2 * i), bv = tex_by_id(801 + 2 * i); os_unfair_lock_unlock(&g_rmu); CHECK(sv.pixelFormat == MTLPixelFormatX32_Stencil8 && sv.parentTexture == base, "a stencil-mode view reads the stencil of the texture's own storage"); CHECK(bv.pixelFormat == MTLPixelFormatX32_Stencil8 && bv.parentTexture == base, "a texture built in stencil mode reads the stencil of the same storage"); CHECK(tex_get(s.serial) == base, "the depth-stencil texture is not replaced"); diff --git a/dm/gfx/tests/t_upload_budget.m b/dm/gfx/tests/t_upload_budget.m new file mode 100644 index 0000000..390d6fb --- /dev/null +++ b/dm/gfx/tests/t_upload_budget.m @@ -0,0 +1,69 @@ +// A streaming burst inside one still-open rendering submission must not retain all +// staging until its rendering fence. Exercise the real Metal upload/retirement path. +#include "../nvn/nvn_mtl.m" +uint64_t g_t0_ns; +int dm_guest_as_contains(uintptr_t address) { (void)address; return 0; } +void *front_metal_layer(void) { return NULL; } +module_t *module_of(const void *p) { (void)p; return NULL; } +module_t g_mod[MAX_MOD]; int g_nmod; +void svc_thread_dump(void) {} +uint64_t now_ns(void) { return bp_now(); } +static int checks, fails; +#define CHECK(c, msg) do { checks++; if (!(c)) { fails++; fprintf(stderr, "FAIL: %s\n", msg); } } while (0) + +static size_t live_bytes(NSHashTable *buffers) { @autoreleasepool { + size_t n = 0; + for (id<MTLBuffer> b in buffers.allObjects) n += b.length; + return n; +} } +static void upload_one(id<MTLTexture> dst, NSHashTable *buffers, uint8_t value) { + up_begin(); + size_t off; + id<MTLBuffer> b = up_alloc(32u << 20, &off); + @autoreleasepool { [buffers addObject:b]; } + [g_up_ce fillBuffer:b range:NSMakeRange(off, 256) value:value]; + up_blit_add(b, off, 256, 256, MTLSizeMake(1, 1, 1), dst, 0, 0); + [g_up_texs addObject:dst]; +} +int main(void) { @autoreleasepool { + mtl_init(); + NSHashTable *buffers = [NSHashTable weakObjectsHashTable]; + NSMutableArray *textures = [NSMutableArray new]; + MTLTextureDescriptor *d = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:1 height:1 mipmapped:NO]; + d.storageMode = MTLStorageModeShared; + d.usage = MTLTextureUsageRenderTarget; + id<MTLTexture> target = [g_dev newTextureWithDescriptor:d]; res_add(target); + mtl_sub_begin(); + MTL4RenderPassDescriptor *pass = [MTL4RenderPassDescriptor new]; + pass.colorAttachments[0].texture = target; + pass.colorAttachments[0].loadAction = MTLLoadActionClear; + pass.colorAttachments[0].storeAction = MTLStoreActionStore; + pass.colorAttachments[0].clearColor = MTLClearColorMake(1, 0, 0, 1); + id encoder = [g_cb renderCommandEncoderWithDescriptor:pass]; + size_t peak = 0; + for (int i = 0; i < 64; i++) { + id<MTLTexture> t = [g_dev newTextureWithDescriptor:d]; + [textures addObject:t]; res_add(t); + upload_one(t, buffers, (uint8_t)(i + 1)); + peak = MAX(peak, live_bytes(buffers)); + } + up_flush(); rec_publish(); up_wait_zero(&g_up_pending, WS_DIAG); + size_t idle = live_bytes(buffers); + CHECK(peak <= (512u << 20), "streaming burst staging stays within 512 MiB"); + CHECK(idle <= (96u << 20), "only three reusable staging blocks survive upload completion before the rendering fence"); + m4_end(encoder); + uint64_t fence = mtl_sub_commit(); + CHECK(mtl_wait(fence, 5000000000ull), "rendering submission completes"); + CHECK(!mtl_last_commit_error(), "no rendering GPU error"); + uint8_t clear[4] = {0}; + [target getBytes:clear bytesPerRow:4 fromRegion:MTLRegionMake2D(0, 0, 1, 1) mipmapLevel:0]; + CHECK(clear[0] == 255 && clear[1] == 0 && clear[2] == 0 && clear[3] == 255, + "the open rendering pass survives upload commits"); + for (int i = 0; i < 64; i++) { + uint8_t pixel[4] = {0}; + [textures[i] getBytes:pixel bytesPerRow:4 fromRegion:MTLRegionMake2D(0, 0, 1, 1) mipmapLevel:0]; + for (int c = 0; c < 4; c++) CHECK(pixel[c] == i + 1, "upload pixels survive batch splitting and staging retirement"); + } + printf("upload budget: peak %zu MiB, completed %zu MiB, %d checks, %d failed\n", peak >> 20, idle >> 20, checks, fails); + return fails != 0; +} } diff --git a/dm/gfx/tests/t_vbuf.m b/dm/gfx/tests/t_vbuf.m index cce412b..4c34295 100644 --- a/dm/gfx/tests/t_vbuf.m +++ b/dm/gfx/tests/t_vbuf.m @@ -42,9 +42,20 @@ int main(void) { @autoreleasepool { memset(mem, 0, size); mem[0] = 11; mem[1] = 22; mem[2] = 33; mem[3] = 99; mtl_pool_add(mem, size); id<MTLBuffer> out = [g_dev newBufferWithLength:16 * 8 options:MTLResourceStorageModeShared]; res_add(out); - state_reset(); S.vbuf[0].address = (uint64_t)mem; S.vbuf[14].address = (uint64_t)(mem + 1); S.vbuf[15].address = (uint64_t)(mem + 2); + state_reset(); + // Exercise the compact record through the production dispatcher before the + // existing shader reads each stream. Its public state remains a full range. + struct { op_hdr h; p_bind_vbuf1 b; } bindings[4] = { + {{OP_BIND_VBUF1,24},{0,4,(uint64_t)mem}}, + {{OP_BIND_VBUF1,24},{14,4,(uint64_t)(mem+1)}}, + {{OP_BIND_VBUF1,24},{15,4,(uint64_t)(mem+2)}}, + {{OP_END,8},{0}} + }; uint64_t other, off; void *mb; mtl_gpu_addr((uint64_t)(mem + 3), 1, &mb, &off, &other); - mtl_sub_begin(); id<MTL4RenderCommandEncoder> en = encoder(ps); + mtl_sub_begin(); mtl_exec((const uint8_t *)bindings); + EXPECT("compact vertex replay restores first full range",1,S.vbuf[0].address==(uint64_t)mem&&S.vbuf[0].size==4); + EXPECT("compact vertex replay restores high stream range",1,S.vbuf[15].address==(uint64_t)(mem+2)&&S.vbuf[15].size==4); + id<MTL4RenderCommandEncoder> en = encoder(ps); bind_vbufs(en); capture(en, out, 0); uint64_t resolves = g_vbuf_resolve, sets = g_vbuf_set; for (int i = 0; i < 1000; i++) bind_vbufs(en); diff --git a/dm/gfx/tests/t_view_levels.m b/dm/gfx/tests/t_view_levels.m index 4f42e86..9439418 100644 --- a/dm/gfx/tests/t_view_levels.m +++ b/dm/gfx/tests/t_view_levels.m @@ -18,7 +18,7 @@ static void view_case(int tid, unsigned level, unsigned count, unsigned expected serial_put(t.serial, base); view_t v = {.set_levels=1, .base_level=level, .nlevels=count}; mtl_reg_tex(tid, &t, &v); - pthread_mutex_lock(&g_rmu); + os_unfair_lock_lock(&g_rmu); id<MTLTexture> view = tex_by_id(tid); CHECK((view != nil) == (expected != 0), "invalid start is empty; valid start creates view"); if (expected) { @@ -27,7 +27,7 @@ static void view_case(int tid, unsigned level, unsigned count, unsigned expected CHECK(view.arrayLength == layers, "array layers preserved"); CHECK(tex_by_id(tid) == view, "cached view preserves range"); } - pthread_mutex_unlock(&g_rmu); + os_unfair_lock_unlock(&g_rmu); } int main(void) { @autoreleasepool { mtl_init(); diff --git a/dm/gfx/tests/t_view_swizzle.m b/dm/gfx/tests/t_view_swizzle.m index 385fff4..2ebe7d8 100644 --- a/dm/gfx/tests/t_view_swizzle.m +++ b/dm/gfx/tests/t_view_swizzle.m @@ -45,11 +45,11 @@ static void swizzle_case(int tid, int format, int view_format, int uploaded, con view_t v = {0}; if (mapping) { v.set_swizzle = 1; memcpy(v.swz, mapping, 4); } if (view_format) { v.set_format = 1; v.format = view_format; } mtl_reg_tex(tid, &t, mapping || view_format ? &v : NULL); - pthread_mutex_lock(&g_rmu); + os_unfair_lock_lock(&g_rmu); id<MTLTexture> view = tex_by_id(tid); CHECK(view != nil, "registered view created"); CHECK(tex_by_id(tid) == view, "cached view retained"); - pthread_mutex_unlock(&g_rmu); + os_unfair_lock_unlock(&g_rmu); read_check(tid, view, expected, 0.001f); } // What Metal itself wrote into a texture of the RGB565 family is in Metal's order: a clear to red reads red, with no swap @@ -64,7 +64,7 @@ static void cleared_case(int tid, int format) { id<MTLCommandBuffer> command = q_cb(); [[command renderCommandEncoderWithDescriptor:rp] endEncoding]; [command commit]; [command waitUntilCompleted]; serial_put(t.serial, base); mtl_reg_tex(tid, &t, NULL); - pthread_mutex_lock(&g_rmu); id<MTLTexture> view = tex_by_id(tid); pthread_mutex_unlock(&g_rmu); + os_unfair_lock_lock(&g_rmu); id<MTLTexture> view = tex_by_id(tid); os_unfair_lock_unlock(&g_rmu); CHECK(view == base, "a cleared RGB565-family texture needs no view"); read_check(tid, view, (const float[8]){1,0,0,1, 1,0,0,1}, 0.001f); } @@ -77,7 +77,7 @@ static void image_case(int tid) { CHECK(base != nil && (base.usage & MTLTextureUsageShaderWrite), "image texture created"); serial_put(t.serial, base); mtl_reg_tex(tid, &t, NULL); - pthread_mutex_lock(&g_rmu); + os_unfair_lock_lock(&g_rmu); id<MTLTexture> view = tex_by_id(tid); uint64_t rid = 0, rid2 = 0; CHECK(view != base && !(view.usage & MTLTextureUsageShaderWrite), "the sampled view is swizzled and read-only"); id<MTLTexture> img = img_fit(tid, view, NVN_DPF_FLOAT, &rid); @@ -85,7 +85,7 @@ static void image_case(int tid) { CHECK(img && (img.usage & MTLTextureUsageShaderWrite) && rid == img.gpuResourceID._impl, "the image slot gets a writable view"); CHECK(s.red == MTLTextureSwizzleRed && s.green == MTLTextureSwizzleGreen && s.blue == MTLTextureSwizzleBlue && s.alpha == MTLTextureSwizzleAlpha, "the image view has no channel mapping"); CHECK(img_fit(tid, view, NVN_DPF_FLOAT, &rid2) == img && rid2 == rid, "the image view is kept for the next bind"); - pthread_mutex_unlock(&g_rmu); + os_unfair_lock_unlock(&g_rmu); run2(pipe_of(@"#include <metal_stdlib>\nusing namespace metal;\nkernel void store(texture2d<float, access::write> t [[texture(0)]], uint i [[thread_position_in_grid]]) { t.write(float4(0.25, 0.5, 0.75, 0.125), uint2(i, 0)); }\n", @"store"), img, nil); uint8_t px[8]; [base getBytes:px bytesPerRow:8 fromRegion:MTLRegionMake2D(0, 0, 2, 1) mipmapLevel:0]; for (int i = 0; i < 8; i++) CHECK(abs((int)px[i] - (int[4]){64, 128, 191, 32}[i % 4]) <= 1, "the image write is stored as written"); diff --git a/dm/gfx/tools/shader_split.py b/dm/gfx/tools/shader_split.py index 1b99a6a..74da417 100644 --- a/dm/gfx/tools/shader_split.py +++ b/dm/gfx/tools/shader_split.py @@ -471,6 +471,37 @@ def cpk_shaders(path, rel, ents, out): print(f'[RED] /{rel}/{e[0]}: {x}; not scanned', flush=True); continue scan_blob(d, f'/{rel}/{e[0]}', out) +ZIP = b'PK\x03\x04' +def zip_jobs(files, parts=64): + """Every member of every zip archive in ranges of about equal length (Portal: the whole game in game.zip, 41,416 members, 4.6 GB inflated, Deflate or stored; its GLSLC outputs are the + 221 nxcontent/nvsb/*.nvsb files, but no name is relied on). → ([(path, rel, (ZIP, members)), …], the zip paths). An archive zipfile cannot read is scanned as one file, as before""" + import zipfile + zs = [] + for p, r in files: + with open(p, 'rb') as f: h = f.read(4) + if h != ZIP: continue + try: + with zipfile.ZipFile(p) as z: zs.append((p, r, [(i.filename, i.file_size) for i in z.infolist() if not i.is_dir()])) + except (zipfile.BadZipFile, OSError, ValueError) as e: print(f'[RED] /{r}: the zip cannot be read ({e}); scanned as one file', flush=True) + jobs, step = [], sum(n for _, _, es in zs for _, n in es) / parts + for p, r, es in zs: + cur, size = [], 0 + for nm, n in es: + cur.append(nm); size += n + if size >= step: jobs.append((p, r, (ZIP, tuple(cur)))); cur, size = [], 0 + if cur: jobs.append((p, r, (ZIP, tuple(cur)))) + return jobs, {p for p, _, _ in zs} + +def zip_shaders(path, rel, names, out): + """A range of zip members (zip_jobs), each inflated and scanned like a loose file: scan_blob, then whole GLSLC outputs (Portal's .nvsb files: a 0x60-byte 'NVSB' header, then one + GLSLC output of a vertex and a pixel shader)""" + import zipfile + with zipfile.ZipFile(path) as z: + for nm in names: + d = z.read(nm); n = len(out) + scan_blob(d, f'/{rel}/{nm}', out) + if len(out) == n and d[:4] != b'MSHD' and b'\x91\x68\x86\x19' in d: out += glslc_shaders(d, f'/{rel}/{nm}', need=False) + def pgdat_members(d, label): """A PlatinumGames DAT archive: 'DAT\\0', u32 file count, then the offsets of five tables: file offsets (u32 each), extensions (4 bytes each), names (u32 name length, then one name of that length each), file lengths (u32 each), a name hash map. Members may be DAT archives again. → [(name, data)], or None when the tables do not fit the data (another game's file that only starts @@ -1581,6 +1612,9 @@ def scan_file(args): if len(args) > 2 and head[:8] == ONEPACK: # a range of Trinity packs (trpfs_jobs) trinity_shaders(path, rel, args[2], found) return rel, uniq_keys(found), var_pairs(found), trees + if len(args) > 2 and head[:4] == ZIP and args[2][0] == ZIP: # members of a zip archive (zip_jobs) + zip_shaders(path, rel, args[2][1], found) + return rel, uniq_keys(found), var_pairs(found), trees if len(args) > 2 and args[2][0] == UE_LEGACY: # files of a legacy Unreal pak (ue_legacy_jobs) ue_legacy_shaders(path, rel, args[2][1], found) return rel, uniq_keys(found), var_pairs(found), trees @@ -1698,8 +1732,9 @@ def main(): arc_jobs, arcs = smash_arc_jobs(files) # Super Smash Bros. Ultimate's data.arc: its current files, in ranges pz_jobs, pkzs = pkz_jobs(files) # Bayonetta 3's PKZ archives, Oodle Kraken decoded by the game's own decoder in the Mac host ck_jobs, cpks = cpk_jobs(files) # Bayonetta 2's CRI CPK archives (CRILAYLA entries) - big += pak_jobs + tr_jobs + ue_jobs + bd_jobs + arc_jobs + pz_jobs + ck_jobs - files = [(p, r) for p, r in files if not linkdata_jobs(p, r) and p not in paks and p not in trp and p not in uep and p not in bdts and p not in arcs and p not in pkzs and p not in cpks] + zp_jobs, zips = zip_jobs(files) # Portal's game.zip (Deflate members) + big += pak_jobs + tr_jobs + ue_jobs + bd_jobs + arc_jobs + pz_jobs + ck_jobs + zp_jobs + files = [(p, r) for p, r in files if not linkdata_jobs(p, r) and p not in paks and p not in trp and p not in uep and p not in bdts and p not in arcs and p not in pkzs and p not in cpks and p not in zips] with cf.ProcessPoolExecutor(J, initializer=_pool_init, initargs=(common, {}, zdicts)) as ex: results += list(ex.map(scan_file, big)) + list(ex.map(scan_file, files, chunksize=16)) trees, defer = {}, [] @@ -1743,7 +1778,7 @@ def main(): for name, rr in recs.items(): with open(os.path.join(pkg, name), 'wb') as f: f.write(b''.join(rr)) with open(mapout, 'w') as f: - kinds = ' / '.join(k for k, hit in (('BNSH / BAHS', any('#BNSH@' in r[4] or '#BAHS@' in r[4] for r in rows)), ('Unity Shader', any('#var' in r[4] and '#BNSH@' not in r[4] and '#MSHD#' not in r[4] and '#SDF#' not in r[4] for r in rows)), + kinds = ' / '.join(k for k, hit in (('BNSH / BAHS', any('#BNSH@' in r[4] or '#BAHS@' in r[4] for r in rows)), ('Unity Shader', any('#var' in r[4] and '#BNSH@' not in r[4] and '#MSHD#' not in r[4] and '#SDF#' not in r[4] and '#GLSLC@' not in r[4] for r in rows)), ('MSHD', any('#MSHD#' in r[4] for r in rows)), ('GLSLC', any('#GLSLC@' in r[4] for r in rows)), ('RE Engine SDF', any('#SDF#' in r[4] for r in rows)), ('MT Framework SPK', any('#MTSPK@' in r[4] for r in rows)), ('MonoGame MGFX', any('#MGFX@' in r[4] for r in rows)), ('PlatinumGames shader objects', any('#PGSO' in r[4] for r in rows)), diff --git a/dm/host/hle_ro.c b/dm/host/hle_ro.c index 5d41444..d6cdfa0 100644 --- a/dm/host/hle_ro.c +++ b/dm/host/hle_ro.c @@ -33,7 +33,10 @@ static module_t *module_from(const ro_module *rm) { HLE_FORCE("_ZN2nn2ro18LookupModuleSymbolEPmPKNS0_6ModuleEPKc", "look up a definition by name in one loaded NRO", Result, h_ro_LookupModuleSymbol, uintptr_t *out, const void *mod, const char *name) { module_t *m = module_from(mod); void *p = m && name ? module_sym(m, name) : NULL; - if (!p) { static int red; if (!red++) LOG("[RED] ro::LookupModuleSymbol %s: %s", name ? name : "(null)", m ? "not defined in that module" : "not a loaded module"); return RO_FAIL; } + // A name the loaded module does not define fails on Horizon too (ldr:ro's not-found result), and games probe for one: Portal's Source engine asks each module for BuiltDebug, which + // only its debug builds define. Only a module the host does not know is wrong + if (!p && m) { static int n; if (n++ < 8) LOG("ro::LookupModuleSymbol %s: not defined in that module (failure, as on Horizon)", name ? name : "(null)"); return RO_FAIL; } + if (!p) { static int red; if (!red++) LOG("[RED] ro::LookupModuleSymbol %s: not a loaded module", name ? name : "(null)"); return RO_FAIL; } if (out) *out = (uintptr_t)p; return 0; } diff --git a/dm/ios/tests/test_xcode_workflow.py b/dm/ios/tests/test_xcode_workflow.py index 98f6565..7e2f784 100644 --- a/dm/ios/tests/test_xcode_workflow.py +++ b/dm/ios/tests/test_xcode_workflow.py @@ -53,7 +53,7 @@ class XcodePipelineTests(unittest.TestCase): 'scan_nsps': (int(registered.id, 16), fam), 'check_prereqs': None, 'free_gb': 10000, 'build_nx_extract': root / 'nx_extract', 'extract': extraction, 'can_clone': False, 'build_mismatch': [], - 'extract_aoc': None, 'rewrite': None, 'split_shaders': 'fixture-key', + 'extract_aoc': None, 'rewrite': profile, 'split_shaders': 'fixture-key', 'build_shaders': root / 'shaders', 'build_vk_shaders': root / 'shaders', 'build_gl_shaders': root / 'shaders', 'provisional_prep': None, 'chain_header': None, 'refine_after_rewrite': profile, 'derive_profile': '1.0.0', 'run': None, diff --git a/dm/tools/tests/test_nro_graphics.py b/dm/tools/tests/test_nro_graphics.py new file mode 100644 index 0000000..d19703b --- /dev/null +++ b/dm/tools/tests/test_nro_graphics.py @@ -0,0 +1,63 @@ +"""What a game's NROs decide (Portal: the Source engine in 28 NROs under an SDL main): the graphics API, the run-time GLSLC capture, and zip members for the shader split.""" +import os +from pathlib import Path +import shutil +import sys +import tempfile +import unittest +import zipfile + +REPO = Path(__file__).resolve().parents[3] +sys.path.insert(0, str(REPO / 'dm/games')); import profile_guess as pg +_had = os.environ.get('DM_GAME'); os.environ['DM_GAME'] = '01007BB017812000' # shader_split loads a profile at import; other tests expect no default game +sys.path.insert(0, str(REPO / 'dm/gfx/tools')); import shader_split as ss +if _had is None: del os.environ['DM_GAME'] +else: os.environ['DM_GAME'] = _had + +SDL_MAIN = {'eglCreateWindowSurface', 'eglGetProcAddress'} # SDL's EGL backend; main names glShaderSource (gl_names) for SDL's GL renderer + + +class NroGraphics(unittest.TestCase): + def test_nvn_in_nros_beats_an_sdl_main(self): + s = pg.shader_sources(SDL_MAIN, 0, ['subsdk0'], 0, 0, gl_names=2, nro_imports={'nvnBootstrapLoader', 'glslcCompile'}) + self.assertEqual(s['graphics'][0], 'nvn') + self.assertTrue(s['glslc_capture'][0]) + self.assertFalse(s['gl_capture'][0]) + + def test_gl_without_nros_unchanged(self): # Digimon Story Cyber Sleuth: no NRO, GL through EGL + s = pg.shader_sources(SDL_MAIN | {'nvnBootstrapLoader'}, 0, [], 0, 0, gl_names=1, gl_files=1) + self.assertEqual(s['graphics'][0], 'gl') + self.assertFalse(s['glslc_capture'][0]) + + def test_nro_glslc_needs_a_module_that_defines_it(self): + s = pg.shader_sources(set(), 0, [], 0, 0, nro_imports={'nvnBootstrapLoader', 'glslcCompile'}) + self.assertEqual(s['graphics'][0], 'nvn') + self.assertFalse(s['glslc_capture'][0]) + + def test_load_order(self): + self.assertEqual(sorted(['sdk', 'subsdk2', 'main', 'subsdk0', 'subsdk10'], key=pg.load_order), ['main', 'subsdk0', 'subsdk2', 'subsdk10', 'sdk']) + + +class ZipMembers(unittest.TestCase): + def setUp(self): + self.tmp = Path(tempfile.mkdtemp()); self.addCleanup(shutil.rmtree, self.tmp) + + def test_members_in_ranges_and_not_scanned_whole(self): + z = self.tmp / 'game.zip' + with zipfile.ZipFile(z, 'w', zipfile.ZIP_DEFLATED) as f: + f.writestr('nxcontent/nvsb/', b'') + for i in range(10): f.writestr(f'nxcontent/nvsb/{i}.nvsb', bytes(100)) + (self.tmp / 'loose.bin').write_bytes(b'not a zip') + jobs, zips = ss.zip_jobs([(str(z), 'game.zip'), (str(self.tmp / 'loose.bin'), 'loose.bin')], parts=4) + self.assertEqual(zips, {str(z)}) + names = [n for _, _, (tag, ns) in jobs for n in ns] + self.assertTrue(all(tag == ss.ZIP for _, _, (tag, _) in jobs)) + self.assertEqual(sorted(names), sorted(f'nxcontent/nvsb/{i}.nvsb' for i in range(10))) # the directory entry is left out + self.assertGreater(len(jobs), 1) + out = [] + for p, r, (_, ns) in jobs: ss.zip_shaders(p, r, ns, out) + self.assertEqual(out, []) # zero-filled members hold no shader + + +if __name__ == '__main__': + unittest.main() diff --git a/docs/agents/symptoms.md b/docs/agents/symptoms.md index ab942d0..9c0004e 100644 --- a/docs/agents/symptoms.md +++ b/docs/agents/symptoms.md @@ -56,11 +56,12 @@ One line per symptom in the [subsystem references](../reference/): what you see - the alias region's mapped total runs past the application pool in the first minute. → [Maps past the application pool fail as on Horizon](../reference/memory.md#maps-past-the-application-pool-fail-as-on-horizon) - a write through a null pointer at +0x1c0 during startup. → [`GetInfo(UserExceptionContextAddress)` returns a real buffer](../reference/memory.md#getinfouserexceptioncontextaddress-returns-a-real-buffer) - a null-pool buffer even with virtual pools working (Virtual memory pools mapped onto one physical pool). → [A null-pool buffer from a game heap that ran out](../reference/memory.md#a-null-pool-buffer-from-a-game-heap-that-ran-out) -- a null-pool buffer for `Model/NpcCommonerA.bfres` in the opening course. → [Per-resource heaps sized from Nintendo's measured peaks](../reference/memory.md#per-resource-heaps-sized-from-nintendos-measured-peaks) -- `main+0x155a7a4` writes through a null `operator new` at the world load, about 1 run in 3. → [Model heaps sized by the host's command record sizes](../reference/memory.md#model-heaps-sized-by-the-hosts-command-record-sizes) +- a model load exhausts its resource heap: a null `operator new` at the world load, or `BufferInitialize: pool has no storage` followed by a null-pool crash. → [Model heaps sized by the host's command record sizes](../reference/memory.md#model-heaps-sized-by-the-hosts-command-record-sizes) - the frames stop at the first loading screen, a graphics worker at 100% CPU (17 s in, `UnityGfxDeviceWorker` running, the main thread waiting on a condition variable, no red line). → [Unity's GPU pool block allocation spins at size 0](../reference/memory.md#unitys-gpu-pool-block-allocation-spins-at-size-0) - SIGBUS inside a host hook (`snprintf` under `mount_ex` / `h_fs_MountSave`), then the process hangs (Super Mario 3D World + Bowser's Fury); SIGBUS inside Metal (`AGCDeserializedReply`, an AGX compiler block) at an address in the guest stack region (Breath of the Wild). → [Host calls on small guest stacks](../reference/memory.md#host-calls-on-small-guest-stacks) - the iPhone app is killed for memory; resident memory grows steadily (about 0.2 MB a frame) while the residency set and the texture counts stay flat. → [Deferred-release drain without an autorelease pool](../reference/memory.md#deferred-release-drain-without-an-autorelease-pool) +- travelling produces a brief multi-GB footprint spike, then memory returns to its earlier level; periodic memory lines can miss the peak. Upload diagnostics show many swapped-out blocks still alive in one batch. → [Upload staging grows within one rendering submission](../reference/memory.md#upload-staging-grows-within-one-rendering-submission) +- GPU memory grows with repeated `nvnTextureReadTexels` calls on a long-lived game thread, even after each readback completes. → [GPU readback without an autorelease pool](../reference/memory.md#gpu-readback-without-an-autorelease-pool) ## Threads @@ -152,6 +153,8 @@ One line per symptom in the [subsystem references](../reference/): what you see - the frame loop runs, but the game waits for code it loads itself (`ro::RegisterModuleInfo` / `ro::GetBufferSize` red lines; `main` imports 1,045 `lua2cpp::create_agent_*` functions, global, that no ExeFS module defines, bound to stubs at start). → [Loading NROs with `ro::LoadModule`](../reference/services.md#loading-nros-with-roloadmodule) - `[EOS] Failed to create platform` (the game plays offline); `refused …: an online SDK (Epic Online Services), not loaded`; `refused an NRR that registers only online SDKs (1 hashes)`. → [Online SDKs in NROs are never loaded](../reference/services.md#online-sdks-in-nros-are-never-loaded) - `main` imports EGL (SDL3's GL support) and `vkGetInstanceProcAddr` (its Vulkan support), but the game renders with NVN (`FNA3D_Driver_NVN.cpp`). → [GPU driver modules that are never loaded](../reference/services.md#gpu-driver-modules-that-are-never-loaded) +- `UNIMPL _ZN5movie12SetAllocatorEPFPvmmS0_EPFvS0_S0_EPFS0_S0_mS0_ES0_`, `UNIMPL glslcInitialize`, then `UNIMPL _ZN5movie9ExtractorC1ENS_13ContainerTypeE` and a crash reading address 0x18 in an NRO (`pc=nro18+0x97f4`) as the first movie starts. → [Modules only the NROs import from](../reference/services.md#modules-only-the-nros-import-from) +- `[RED] ro::LookupModuleSymbol BuiltDebug: not defined in that module` once at boot; nothing goes wrong after it. → [`ro::LookupModuleSymbol` for a name the module does not define](../reference/services.md#rolookupmodulesymbol-for-a-name-the-module-does-not-define) - harmless on the build it was written for, silent memory corruption on any other. → [Code inherited from another title at fixed addresses](../reference/services.md#code-inherited-from-another-title-at-fixed-addresses) ## Movies @@ -211,6 +214,8 @@ One line per symptom in the [subsystem references](../reference/): what you see - a steady game whose frame-rate counter wanders between 50 and 60 on the iPhone only; the `display link up to tick` lines show about as many empty ticks as frames dropped by merging: frames arrive two in one tick and none in the next. → [Vulkan acquire and vsync event follow the display link](../reference/frame-rate.md#vulkan-acquire-and-vsync-event-follow-the-display-link) - asked for 60 Hz, the system fires the display link on every third refresh of a 165 Hz screen, 55 Hz, so games paced by it ran at 55 fps (27.5 at 30). → [Virtual 60 Hz ticks on screens that are not a multiple of 60](../reference/frame-rate.md#virtual-60-hz-ticks-on-screens-that-are-not-a-multiple-of-60) - virtual ticks keep the average, but a frame still stays up two or three refreshes in turn (13.9 / 20.8 ms at 144 Hz), and Apple's HUD showed the frame interval jumping between them. → [Fullscreen switches the screen to a rate the frame rate divides](../reference/frame-rate.md#fullscreen-switches-the-screen-to-a-rate-the-frame-rate-divides) +- autorelease work takes a measurable share of replay time, with temporary arena-buffer ownership traffic a few times per draw. → [Arena buffer ownership on allocation](../reference/frame-rate.md#arena-buffer-ownership-on-allocation) +- the submit thread waits on the texture / sampler registry lock while a lower-priority registration thread is preempted holding it. → [Registry lock priority inheritance](../reference/frame-rate.md#registry-lock-priority-inheritance) - the GPU sits idle each frame while the host translates the submission (a game that does not submit frame N+1 until frame N's GPU work is done). → [Submissions handed to the GPU in segments](../reference/frame-rate.md#submissions-handed-to-the-gpu-in-segments) - the submit thread's CPU samples dominated by `mtl_addr` / `mtl_gpu_addr` (vertex buffers in `draw`, uniform buffers in `encode_args`). → [Address to memory pool lookups by binary search](../reference/frame-rate.md#address-to-memory-pool-lookups-by-binary-search) - stalls of 30–60 ms about once a second, at 30 fps and at 60 (99th percentile 63–127 ms); the main thread waits 20–34 ms for the render thread once a second. → [Time-constraint computation declared below the thread's peaks](../reference/frame-rate.md#time-constraint-computation-declared-below-the-threads-peaks) @@ -331,6 +336,7 @@ One line per symptom in the [subsystem references](../reference/): what you see - GX points and lines drawn as one-pixel points and lines; a red line for the sampler LOD bias. → [Not translated in the Vulkan layer: geometry shaders and LOD bias](../reference/rendering.md#not-translated-in-the-vulkan-layer-geometry-shaders-and-lod-bias) - `UNIMPL eglGetDisplay`, then `Break` right after `nv::InitializeGraphics` and `vi::OpenDefaultDisplay` (`main+0x5ed9d0`). → [Games that render with desktop OpenGL through EGL](../reference/rendering.md#games-that-render-with-desktop-opengl-through-egl) - the NSP script's provisional profile said Vulkan, and the capture boot crashed in the GL layer. → [Telling a GL game from an NVN or Vulkan one](../reference/rendering.md#telling-a-gl-game-from-an-nvn-or-vulkan-one) +- the NSP script took the game for an OpenGL one (`main imports EGL and names glShaderSource`), the GL shader capture found no shader, and the capture boot stopped with `[nvn-mtl] FATAL cannot read the shader map` from `h_nvnQueueSubmitCommands` called in an NRO (`nro8+0xc10d0`). → [NVN in NROs under an SDL main that names GL](../reference/rendering.md#nvn-in-nros-under-an-sdl-main-that-names-gl) - a jump to address 0 right after `glGetFloatv(0xb12)` (`lr=main+0x2d58c4`). → [Which GL functions a game calls](../reference/rendering.md#which-gl-functions-a-game-calls) - in a capture run every program links with no uniforms; the game's first `Shader:send` raises a Lua error and, on the Switch, its error handler quits (Any Lua error quits a LÖVE game). → [GL shaders put together at run time and captured](../reference/rendering.md#gl-shaders-put-together-at-run-time-and-captured) @@ -339,6 +345,7 @@ One line per symptom in the [subsystem references](../reference/): what you see - `shader_split.py` finds 0 shaders in a RomFS of zstd files named `.zst`. → [Zstd files recognised by their magic](../reference/shaders.md#zstd-files-recognised-by-their-magic) - a provisional split finds only a fraction of the shaders (3,126 of Pokémon Sword's 41,516). → [BNSH entries of GFPAK archives](../reference/shaders.md#bnsh-entries-of-gfpak-archives) - the split found no shader in the game (0 shaders). → [GLSLC outputs holding several stages](../reference/shaders.md#glslc-outputs-holding-several-stages) +- an NVN game's split finds none of its shipped shaders, and the ones it compiles at run time were not captured: the game's renderer is in NROs that call `glslcCompile`, and its whole RomFS is one zip. → [GLSLC outputs in a zip archive, and those NROs compile at run time](../reference/shaders.md#glslc-outputs-in-a-zip-archive-and-those-nros-compile-at-run-time) - 24 shaders before the control-length fix, 1,355 after. → [SPKG packages in KTGL LINKDATA](../reference/shaders.md#spkg-packages-in-ktgl-linkdata) - `[RED] ... PKZ version 0x10000: only 0x20000 is known` stops the shader split. → [PKZ version 1 archives](../reference/shaders.md#pkz-version-1-archives) - the split finds no shaders, although `main` contains NVN control sections; it reports that no data record or code-address pair identifies their code areas. → [Needle shader containers in main and GFD archives](../reference/shaders.md#needle-shader-containers-in-main-and-gfd-archives) diff --git a/docs/images/portal.jpg b/docs/images/portal.jpg new file mode 100644 index 0000000..3e45758 Binary files /dev/null and b/docs/images/portal.jpg differ diff --git a/docs/reference/frame-rate.md b/docs/reference/frame-rate.md index dcec665..f558c9b 100644 --- a/docs/reference/frame-rate.md +++ b/docs/reference/frame-rate.md @@ -89,6 +89,22 @@ Frame pacing: the host's frame step, engine frame limiters, present intervals an ## CPU cost and pacing +### Arena buffer ownership on allocation + +- **Symptom:** autorelease work takes a measurable share of replay time, with temporary arena-buffer ownership traffic a few times per draw. +- **Cause:** under ARC an unqualified `id<MTLBuffer> *` out-parameter is `__autoreleasing`, so assigning the arena buffer in `arena_alloc` autoreleases it. Reading `buf.length` also sends an Objective-C message on each allocation and while selecting or trimming arenas. +- **Fix:** both declarations of `arena_alloc` use a `__strong` buffer out-parameter, matching every caller's strong local. `NvArena` caches its buffer length when created; allocation, reuse selection and idle trimming use that value. Buffer ownership and fence-based reuse remain the same. +- **Found on:** MacNativeGames commit `0531f28`, ported in [PR #195](https://github.com/seedds/nxnative/pull/195), reports autorelease work falling from 2.63% to 1.73% of replay time; the PR does not identify the game for that measurement. It has not been repeated here. +- **Check:** `t_ub`, `t_vbuf` and `t_geometry` exercise arena-backed uniform shadows, vertex conversion and geometry scratch on the Mac with Metal validation. + +### Registry lock priority inheritance + +- **Symptom:** the submit thread waits on the texture / sampler registry lock while a lower-priority registration thread is preempted holding it. +- **Cause:** game threads register textures and samplers under `g_rmu`, which the submit thread also takes for argument encoding. The previous mutex allowed those owners to delay the higher-priority submitter. +- **Fix:** `g_rmu` is an `os_unfair_lock`, lending a waiter's priority to its owner. All registration, lookup, cache, diagnostic and test callers use its lock API; the critical sections and lock order stay the same. +- **Found on:** [The Legend of Zelda: Tears of the Kingdom](../../dm/games/0100F2C0115B6000/README.md), measured upstream in MacNativeGames commit `5f5de5e` and ported in [PR #195](https://github.com/seedds/nxnative/pull/195): in a shrine, game threads at priority 46 delayed the submit thread at priority 97 by about 1.8 ms a frame. Waiting fell from 155 to about 0 ms/s there, with unchanged frame time. Those game measurements have not been repeated here. +- **Check:** `t_stencil_view`, `t_view_levels` and `t_view_swizzle` exercise production registry lookups and cached views with the new lock on the Mac. + ### Submissions handed to the GPU in segments - **Symptom:** the GPU sits idle each frame while the host translates the submission (a game that does not submit frame N+1 until frame N's GPU work is done). diff --git a/docs/reference/memory.md b/docs/reference/memory.md index 0c0078c..92bc0ad 100644 --- a/docs/reference/memory.md +++ b/docs/reference/memory.md @@ -232,21 +232,13 @@ The guest address space, heaps, pools, virtual address memory and the allocators - **Note:** read the probe log carefully: `tryAlloc` probes show *requests*, not successes, so a running total that already exceeds the heap means an earlier request failed. - **Found on:** [Super Mario Bros. Wonder](../../dm/games/010015100B514000/README.md), [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md) (the two entries below). -### Per-resource heaps sized from Nintendo's measured peaks - -- **Symptom:** a null-pool buffer for `Model/NpcCommonerA.bfres` in the opening course. -- **Cause:** each resource gets its own `sead::ExpHeap` sized *resource-size-table value + 0x1008* (`main+0x1e603c` → `ExpHeap::create` `main+0x4aac38`). The table values are Nintendo's measured peaks with almost no margin (Yoshi's animation file uses 99 % of its budget). `Model/NpcCommonerA.bfres` came up about 0x70 bytes short: its last GPU chunk (0x9000 bytes, 4 KB aligned) fit, but the chunk's 0x598-byte descriptor did not. -- **Fix:** a registered code cut gives every resource heap +16 KB (`CUTS` in the profile's `patches.py`, keyed by the module build ID, original bytes verified). Per-title code patches go there, with the reason; those of Nintendo's `sdk` modules in `dm/tools/sdk_patches.py`. -- **Note:** Breath of the Wild's identical failure came from sizes the game takes from the host (next entry); whether Mario's chunk is sized the same way is not checked yet. -- **Found on:** [Super Mario Bros. Wonder](../../dm/games/010015100B514000/README.md). - ### Model heaps sized by the host's command record sizes -- **Symptom:** `main+0x155a7a4` writes through a null `operator new` at the world load, about 1 run in 3. -- **Cause:** each model's heap is its table entry + 0x1008, the file goes in 4 KB aligned, and the setup (`main+0x155a2ac`) takes the rest as a frame heap (vtable `main+0x1d4f950`, head `+0xe0`, tail `+0xe8`). Its use is the same in every run; only the file's alignment pad varies (0 to 0xff8), and the entries leave room for any pad, about 0xd00 to spare on most models. Three models were a 4 KB page short, so the loads with a large pad failed. The game sizes the model's prerecorded commands by recording each mesh's draw and each shape's per-pass state into a scratch command buffer and summing `GetCommandMemoryUsed` (`main+0xf9aeb4`, `main+0xf9bb54`), and our records were larger than the driver's (draws carried the indirect and multi-draw fields, every recording began with a BEGIN record). -- **Fix:** plain draws now leave those fields out and recordings start with their first command (`hle_nvn.c`; [What `GetCommandMemoryUsed` counts](nvn-objects.md#what-getcommandmemoryused-counts)). -- **Check:** to check a whole scene, probe where the setup heap is cut (`main+0x155a520` with `!x21`, the resource heap) and the end of the setup (`main+0x155a5f4` with `!x2`, the frame heap), with `DM_PROBE_HEAP=28,38,40,e0,f8 DM_PROBE_HEAPFRAME=1d4f950 DM_PROBE_HEAPWORDS=1` ([05](../agents/05-debugging.md#probes-without-a-debugger-dm_probe)). A model fits any placement when entry + 0x1008 covers the resource heap's header and object (0x310), the file, the frame heap's block header and its peak use, plus 0xff8 of pad. -- **Found on:** [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md). +- **Symptom:** a model load exhausts its resource heap: a null `operator new` at the world load, or `BufferInitialize: pool has no storage` followed by a null-pool crash. +- **Cause:** games size model allocations by measuring scratch command recordings; oversized host records can exceed the budgets in their resource-size tables. In Breath of the Wild, each model's heap is its table entry + 0x1008, the file goes in 4 KB aligned, and the setup (`main+0x155a2ac`) takes the rest as a frame heap (vtable `main+0x1d4f950`, head `+0xe0`, tail `+0xe8`). Its use is the same in every run; only the file's alignment pad varies (0 to 0xff8), and the entries leave room for any pad, about 0xd00 to spare on most models. Three models were a 4 KB page short, so the loads with a large pad failed. The game sizes the model's prerecorded commands by recording each mesh's draw and each shape's per-pass state into a scratch command buffer and summing `GetCommandMemoryUsed` (`main+0xf9aeb4`, `main+0xf9bb54`), and our records were larger than the driver's (draws carried the indirect and multi-draw fields, every recording began with a BEGIN record). +- **Fix:** plain draws leave unused indirect fields out and recordings start with their first command. Uniform and single vertex-buffer bindings also use compact 24-byte records instead of 32, preserving full GPU addresses and retaining full-width fallback records for ranges above `UINT32_MAX`; multi-vertex ranges retain their array representation. Replay expands the compact forms into the same canonical binding state. The native inline uniform command takes 20 bytes and the native vertex-buffer command 24, so these small differences can accumulate into an extra 4 KB GPU-pool chunk. Report the bytes actually recorded; do not underreport usage or add a game-specific heap allowance. See [What `GetCommandMemoryUsed` counts](nvn-objects.md#what-getcommandmemoryused-counts). +- **Check:** for Breath of the Wild's whole scene, probe where the setup heap is cut (`main+0x155a520` with `!x21`, the resource heap) and the end of the setup (`main+0x155a5f4` with `!x2`, the frame heap), with `DM_PROBE_HEAP=28,38,40,e0,f8 DM_PROBE_HEAPFRAME=1d4f950 DM_PROBE_HEAPWORDS=1` ([05](../agents/05-debugging.md#probes-without-a-debugger-dm_probe)). A model fits any placement when entry + 0x1008 covers the resource heap's header and object (0x310), the file, the frame heap's block header and its peak use, plus 0xff8 of pad. +- **Found on:** [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md), [Super Mario Bros. Wonder](../../dm/games/010015100B514000/README.md#memory) (buffer bindings; the game note records the failed allocation, measured sizes and validation). ### Unity's GPU pool block allocation spins at size 0 @@ -270,3 +262,19 @@ The guest address space, heaps, pools, virtual address memory and the allocators - **Fix:** `defer_drain` has its own `@autoreleasepool`. - **Found on:** [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md): GitHub issue #9 (iPhone 16 Pro Max, handheld), killed in the first shrine. An 8 GB iPhone gives this app about 3.4 GB (resident plus `headroom to the system memory limit` stayed at 3,375 MB); resident memory grew from 2.6 GB at the shrine load until the kill. On the Mac a host without that pool grew 1 MB a frame in the shrine (GPU 3.0 → 4.4 GB in 2,400 frames); with it the shrine stays at 2.7–2.9 GB resident and 1.5–1.7 GB GPU, idle or walking (vmmap at 2.8 GB: about 1.6 GB guest memory, 0.75 GB private textures, 0.17 GB IOAccelerator). - **Note:** found on the way: dead texture serials and the fixed-size pool tracking table ([Dead texture serials kept in a ghost table](nvn-objects.md#dead-texture-serials-kept-in-a-ghost-table), [Memory-pool tracking table grows in blocks](nvn-objects.md#memory-pool-tracking-table-grows-in-blocks)). + +### Upload staging grows within one rendering submission + +- **Symptom:** travelling produces a brief multi-GB footprint spike, then memory returns to its earlier level; periodic memory lines can miss the peak. Upload diagnostics show many swapped-out blocks still alive in one batch. +- **Cause:** `up_alloc` could append unlimited staging blocks before `up_flush`. Overflow staging then retired on the main rendering fence, so splitting uploads alone still retained completed staging while that submission was being built. Weak-table insertion and enumeration in the staging diagnostics also left temporary strong references in the submission's autorelease pool. +- **Fix:** split GPU deswizzle batches before another block would exceed 128 MiB. The three upload allocator slots provide backpressure, publishing deferred commits before waiting. Retire overflow and CPU-upload staging from the upload completion handler, remove residency and clear its retained array before freeing the slot; keep diagnostic weak-table operations in their own autorelease pools. Rendering destinations keep their existing lifetime. A single texture larger than the batch budget still gets its own block; this is not a cap on all renderer memory. +- **Check:** `make -C dm/gfx/tests GAME=<id> check_t_upload_budget` uses actual Metal uploads during an open render pass, with direct and deferred encoding and Metal validation. A 2 GiB burst retained 2,048 MiB before the fix; afterwards it peaks at 384 MiB and retains only the three reusable blocks (96 MiB) after upload completion, before the rendering fence. It also checks every uploaded pixel and the render pass's clear. For a game, capture both frequent `phys_footprint` samples and the kernel's peak ledger; the existing `resident` log label denotes footprint, and staging blocks `created in total` is cumulative, not live memory. +- **Found on:** [The Legend of Zelda: Tears of the Kingdom](../../dm/games/0100F2C0115B6000/README.md#memory), investigating issue #147. Mac reproduction and device validation are tracked in the game note. + +### GPU readback without an autorelease pool + +- **Symptom:** GPU memory grows with repeated `nvnTextureReadTexels` calls on a long-lived game thread, even after each readback completes. +- **Cause:** the GPU path in `mtl_read_texels` runs outside a submission's autorelease pool. Metal's temporary command buffer and blit encoder keep the readback buffer alive in the calling thread's undrained pool. +- **Fix:** `mtl_read_texels` encloses texture resolution, readback encoding, the GPU wait and the CPU copy in its own `@autoreleasepool`. The guest-memory path needs no Metal objects. +- **Found on:** [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md), measured upstream in MacNativeGames commit `635fb31` and ported in [PR #195](https://github.com/seedds/nxnative/pull/195): about 130 readbacks a second retained 3.7 GB of GPU memory after 30 minutes. Those game measurements have not been repeated here. +- **Check:** `make -C dm/gfx/tests GAME=010015100B514000 check_t_readback` checks literal GPU and CPU readback pixels and observes the real readback buffer through a weak reference while the caller's outer pool remains open. Removing only the local pool leaves that buffer alive and fails the lifetime check. diff --git a/docs/reference/rendering.md b/docs/reference/rendering.md index 3a462ec..a784f11 100644 --- a/docs/reference/rendering.md +++ b/docs/reference/rendering.md @@ -643,6 +643,13 @@ Digimon Story Cyber Sleuth: Complete Edition (Media.Vision's MVGL engine, SDK 6. - **Found on:** [Digimon Story Cyber Sleuth: Complete Edition](../../dm/games/010014E00DB56000/README.md): `main` imports `nvnBootstrapLoader` and 13 EGL functions. Its graphics setup (`main+0x5ed8f0`) makes an NVN device first, which only a helper beside the movie player uses (`main+0x528000`–`+0x52c400`: a memory pool and four textures next to the movie's GL textures). Then `eglGetDisplay`, `eglBindAPI(EGL_OPENGL_API)`, a 4.5 core context, a window surface on `vi::CreateLayer(w, h)` and a glad loader (`main+0x52c460`). Of the 22 games extracted here (at the time), only Digimon matches the rule. [Balatro](../../dm/games/0100CD801CE5E000/README.md) (SDK 18.3.2, LÖVE 11.4 on OpenGL with SDL and LuaJIT; one boot): `main` imports the EGL functions and `vkGetInstanceProcAddr`, SDL's Vulkan loader, which nothing calls (`subsdk1` is NVIDIA's Vulkan driver); Hades and both Galaxies import 99 or 100 Vulkan functions, `vkCreateInstance` among them. - **Note:** [Absolum](../../dm/games/0100A83022A22000/README.md) imports the same EGL functions (SDL3) but renders with NVN and names no GL function. +### NVN in NROs under an SDL main that names GL + +- **Symptom:** the NSP script took the game for an OpenGL one (`main imports EGL and names glShaderSource`), the GL shader capture found no shader, and the capture boot stopped with `[nvn-mtl] FATAL cannot read the shader map` from `h_nvnQueueSubmitCommands` called in an NRO (`nro8+0xc10d0`). +- **Cause:** Portal's `main` is SDL with its EGL video backend; it imports 20 EGL functions and names `glShaderSource` twice (SDL's GL renderer), but imports no NVN and draws nothing. The renderer is in the Source engine NROs: `materialsystem.nro` (with the D3D9 layer linked in) and `video_nx.nro` import `nvnBootstrapLoader` and draw with NVN. +- **Fix:** `profile_guess.py` `shader_sources` now reads the imports of the loose NROs in the RomFS (`romfs_nro_imports`): when one imports `nvnBootstrapLoader`, the game is an NVN game whatever `main` imports. Digimon keeps the GL rule: it has no NRO. +- **Found on:** [Portal](../../dm/games/01007BB017812000/README.md). + ### Which GL functions a game calls - **Symptom:** a jump to address 0 right after `glGetFloatv(0xb12)` (`lr=main+0x2d58c4`). diff --git a/docs/reference/rewriter.md b/docs/reference/rewriter.md index 7297b7c..9c14e9c 100644 --- a/docs/reference/rewriter.md +++ b/docs/reference/rewriter.md @@ -11,13 +11,13 @@ The NSO rewriter (`dm/tools/patch_nso.py`) and its per-build tables (`dm/tools/s - **Fix:** a profile can opt individual cuts into `MATCH_CUTS` in its `patches.py`. The shared matcher, `dm/tools/nso_match.py`, selects a unique complete unwind function by length and SHA-256 fingerprint. It retains constants, registers and internal branch destinations; normalizes relocation addresses; and checks the normalized bodies of direct callees or their imported symbol names. Original bytes are checked again before a cut is applied. Missing boundaries, unknown callees, changed code and ambiguous matches produce a red diagnostic and no cut. Overlapping rules stop the rewrite. Every accepted cut is recorded in `<module>.cuts.tsv` with `[function match]` in its reason. - **Scope:** the repository profile selected by `DM_GAME`, keyed by module name. A derived profile still uses that repository profile's rules. Other games' rules never participate. A build with registered `CUTS` uses those instead, including an explicitly empty list. SDK patches, observation gates, frame-step stubs and fixed host addresses retain their existing build-ID gates. The rewrite cache includes the selected profile's patch file even for unknown builds, the selected game ID and the matching tool. - **Limits:** this is an opt-in code-identity check, not automatic compatibility for every game update. Direct callee bodies are checked one level deep; their callees, indirect-call targets and external data layouts are not proven unchanged. Enroll only a cut whose assumptions remain valid under those changes. Prefer fixing the general host behavior when possible. A replacement must contain no PC-relative instruction; relocated branch replacements need a different mechanism. Failure leaves the game potentially without its workaround, and stays visible in the rewrite log. -- **Found on:** [Super Mario Bros. Wonder](../../dm/games/010015100B514000/README.md#memory), whose resource-heap allowance was missing on 1.0.1. +- **Found on:** [Super Mario Bros. Wonder](../../dm/games/010015100B514000/README.md#memory), whose former resource-heap allowance was missing on 1.0.1. The shared matcher remains, but Wonder's allowance and matching rule were later removed after the general command-record fix described in its game note. To enroll a cut for any game's own module: 1. Start from a verified fixed cut and its original NSO. Run `python3 dm/tools/nso_match.py <module.nso> <offset> <old hex> <new hex> <reason>` with the tools' Python environment. It emits a rule containing `name`, `size`, `fingerprints` and function-relative `cuts`, and rejects an ambiguous reference. Keep game files and scratch outputs in the game's NSP work folder. 2. Check the function and callees in another known build using `nso_fnmap.py` and disassembly. Check that the cut still has the same effect. `Image.match(rule)` must resolve the reviewed location. If a reviewed implementation differs, generate its fingerprint separately; combine fingerprints only when function size and relative cuts agree. Retain exact `CUTS` for known builds. -3. Add the rule under `MATCH_CUTS = {'main': [rule]}` (or the appropriate non-SDK module) in that game's `patches.py`. Record the evidence and remaining runtime validation in the game's note. Do not copy Wonder's rule or allowance into unrelated games. +3. Add the rule under `MATCH_CUTS = {'main': [rule]}` (or the appropriate non-SDK module) in that game's `patches.py`. Record the evidence and remaining runtime validation in the game's note. Do not transplant a workaround into unrelated games. 4. Verify an unregistered build-ID copy through the rewriter, comparing its output with the known fixed cut, and run `test_nso_match.py`, `test_patches.py` and `test_nro_pack.py`. The matching tests use synthetic modules from two profiles to check relocation, rejection and isolation without game files. ## Jump tables diff --git a/docs/reference/services.md b/docs/reference/services.md index d5cce6c..ab4b107 100644 --- a/docs/reference/services.md +++ b/docs/reference/services.md @@ -588,6 +588,20 @@ Most service calls can be found before they crash, before the first boot or the - **Fix:** they are never loaded (`profile_guess.py` `GPU_DRIVERS`); the EGL imports stay unbound stubs that nothing calls. - **Found on:** [Absolum](../../dm/games/0100A83022A22000/README.md) (FNA3D). +### Modules only the NROs import from + +- **Symptom:** `UNIMPL _ZN5movie12SetAllocatorEPFPvmmS0_EPFvS0_S0_EPFS0_S0_mS0_ES0_`, `UNIMPL glslcInitialize`, then `UNIMPL _ZN5movie9ExtractorC1ENS_13ContainerTypeE` and a crash reading address 0x18 in an NRO (`pc=nro18+0x97f4`) as the first movie starts. +- **Cause:** the provisional profile loads a subsdk only when `main` imports something from it that the host does not supply. The survey extracts only the code modules, so the NROs in the RomFS were not looked at. Portal's `main` is a launcher with SDL; its Source engine NROs play the movies through NintendoSDK_movie (`subsdk2`, imported by `video_nx.nro`) and initialize and call GLSLC (`subsdk0`, imported by `materialsystem.nro` and `video_nx.nro`). Left out, their imports were bound to stubs, and the stub constructor of `movie::Extractor` left the object empty. +- **Fix:** once the rewrite stage has collected the NROs, the NSP script adds to a provisional profile every subsdk whose exports an NRO imports and that no loaded module, other NRO or host hook supplies (`profile_guess.py` `nro_modules`, written under a section `After the NRO collection`). NVIDIA's GPU drivers stay out, as above. +- **Found on:** [Portal](../../dm/games/01007BB017812000/README.md) (`modules main subsdk0 subsdk2 sdk`; `subsdk1`, NVIDIA's OpenGL driver, stays out). + +### `ro::LookupModuleSymbol` for a name the module does not define + +- **Symptom:** `[RED] ro::LookupModuleSymbol BuiltDebug: not defined in that module` once at boot; nothing goes wrong after it. +- **Cause:** the Source engine asks each module it loads for `BuiltDebug`, which only its debug builds define. `ldr:ro` fails such a lookup on Horizon too, and the game takes the failure as "release build". +- **Fix:** the host's answer was already Horizon's (a failure result). The line is no longer red when the module is a loaded one; a lookup in a module the host does not know stays red (`hle_ro.c`). +- **Found on:** [Portal](../../dm/games/01007BB017812000/README.md). + ### IL2CPP common string table intact - Not needed in the end: the tolerant common string table of pull request #15. [Ace Attorney Investigations Collection](../../dm/games/010005501E68C000/README.md) 1.0.0.2's `main` has the table intact at `main+0x46169ac`, 109 entries ending at `Hash128`. diff --git a/docs/reference/shaders.md b/docs/reference/shaders.md index 7608920..58af6c5 100644 --- a/docs/reference/shaders.md +++ b/docs/reference/shaders.md @@ -66,6 +66,13 @@ Finding, keying and translating shaders (`dm/gfx/tools/shader_split.py`, `dmsh_b - **Fix:** such an output is read by its section headers (+0x4c section count, section *i*'s header at +0x90 + 0x90 *i*, type 0 = GPU code; `glslc_program`; the same headers SDK 4's outputs are read through, [GLSLC outputs read through their section headers](#glslc-outputs-read-through-their-section-headers)), and its stages are paired with each other, not as single-stage programs. The NSP script now stops at the split when it finds none. - **Found on:** Baba Is You (no profile). +### GLSLC outputs in a zip archive, and those NROs compile at run time + +- **Symptom:** an NVN game's split finds none of its shipped shaders, and the ones it compiles at run time were not captured: the game's renderer is in NROs that call `glslcCompile`, and its whole RomFS is one zip. +- **Cause:** Portal keeps all its files in `game.zip` (41,416 members, Deflate or stored). The Source engine's D3D9 layer (`materialsystem.nro`) links each vertex and pixel shader combination into one program; the programs of the game's precache list (`portal/glbaseshaders.cfg`, 219 entries) ship compiled as `nxcontent/nvsb/<hash>.nvsb` (`NVSB`, version 0x65, the file's length at +8, a 0x60-byte header, then one GLSLC output holding a vertex and a pixel shader). A deflated member hides the GLSLC magic from the loose-file scan. The game compiles the rest with `glslcCompile` (its movie shaders and its own blits: 10 outputs within 30 s of boot), and the capture rule looked only for `main` holding GLSL sources and importing `glslcCompile`. +- **Fix:** `shader_split.py` reads zip archives member by member in parallel ranges (`zip_jobs` / `zip_shaders`; `zipfile`), each member scanned like a loose file, whole GLSLC outputs included. `profile_guess.py` `shader_sources` asks for the run-time GLSLC capture when an NRO the game loads imports `glslcCompile` and a subsdk defines it (`glslc_capture`). Portal's map: 242 keys from the `.nvsb` files and 13 from the capture, 229 vertex and pixel pairs. +- **Found on:** [Portal](../../dm/games/01007BB017812000/README.md). No other profile's RomFS has a zip archive holding shaders. + ### G2S containers in KTGL RDB archives - **How it is stored:** each material's shaders are a G2S container (`_S2G3000`, type KTID `0x7bcd279f`; 23,108 entries in `asset/system.rdb.bin*`, 2.3 GB inflated) of GLSLC outputs, as in 13 Sentinels' `main`, inside zlib-chunked IDRK entries ([01](../agents/01-extract.md)). One `_DOK0000` container (type `0x20a6a0bb`, the engine's system resources) holds 151 more bare GLSLC outputs, 21 of them drawn in the first battle. diff --git a/scripts/build_ipa_from_nsp.py b/scripts/build_ipa_from_nsp.py index fa6f9cf..4634f0e 100755 --- a/scripts/build_ipa_from_nsp.py +++ b/scripts/build_ipa_from_nsp.py @@ -876,15 +876,28 @@ def collect_nros(py, repo, romfs, dst, env, logs): return {l.split('\t')[0]: dst / (l.split('\t')[0] + '.nro') for l in (dst / 'nro.tsv').read_text().splitlines() if l and not l.startswith('#') and not l.startswith('refused\t')} -def rewrite(py, repo, exe_dir, out, env, logs, g): +def nro_modules(g, exe_dir, nros): + """modules of a provisional profile once the NROs are collected: the subsdks the NROs import from, which the survey could not see (dm/games/profile_guess.py nro_modules; Portal's movie + module and GLSLC). Returns the profile, reloaded when it changed""" + add = pg.nro_modules(exe_dir, nros, g.words('modules')) + if not add: return g + mods = sorted(set(g.words('modules')) | set(add), key=pg.load_order) + why = ', '.join(f'{m} ({k})' for m, k in add.items()) + say(f'modules {" ".join(mods)}: the NROs the game loads import from {why}') + return set_keys(g, {'modules': (' '.join(mods), f'the NROs the game loads at run time import from {why}, which main does not')}, 'After the NRO collection') + + +def rewrite(py, repo, exe_dir, out, env, logs, g, provisional=False): """patch_nso.py on every code module (each writes its own <module>.* files, all the same build_src.sha256 line), each with its log, as many at once as the Mac has cores. - The modules are the profile's and the NROs the game registers (collect_nros; nro.tsv beside them lists those). Reused when the modules, patch_nso.py (with nso_code.py, nso_asm.py - and the selected profile's function rules / the patch files of their builds), its switches and capstone are as at the last successful rewrite (Monster Hunter Rise's main alone takes 5.5 min)""" + The modules are the profile's and the NROs the game registers (collect_nros; nro.tsv beside them lists those); a provisional profile first gets the subsdks those NROs import from (nro_modules). + Reused when the modules, patch_nso.py (with nso_code.py, nso_asm.py and the selected profile's function rules / the patch files of their builds), its switches and capstone are as at the + last successful rewrite (Monster Hunter Rise's main alone takes 5.5 min). Returns the profile""" import capstone - mods = g.words('modules') PROG.stage('rewrite', step='checking the inputs') nro_dir = out.with_name('nro_' + out.name.removeprefix('patched_')) nros = collect_nros(py, repo, exe_dir.parent / 'RomFS', nro_dir, env, logs) + if provisional and nros: g = nro_modules(g, exe_dir, nros) + mods = g.words('modules') manifest = nro_dir / 'nro.tsv' src = {m: exe_dir / m for m in mods} | nros names = list(src) @@ -892,7 +905,7 @@ def rewrite(py, repo, exe_dir, out, env, logs, g): key = fingerprint(('files', [repo / 'dm/tools' / f for f in ('patch_nso.py', 'nso_code.py', 'nso_asm.py', 'nso_match.py')] + patch_files(repo, exe_dir, mods, env.get('DM_GAME')) + list(src.values()) + ([manifest] if manifest.is_file() else [])), names, capstone.__version__, {k: env.get(k) for k in ('DM_PATCH_FRONT', 'DM_GAME')}) if all((out / f'{m}.patched.bin').is_file() for m in names) and reused(kf, key) is not None: - PROG.end(f'{", ".join(mods)}{f" and {len(nros)} NROs" if nros else ""}, reused (inputs unchanged)'); return + PROG.end(f'{", ".join(mods)}{f" and {len(nros)} NROs" if nros else ""}, reused (inputs unchanged)'); return g tmp = out.with_name(out.name + '.partial'); rmtree(tmp); tmp.mkdir(parents=True); logs.mkdir(parents=True, exist_ok=True) if manifest.is_file(): shutil.copyfile(manifest, tmp / 'nro.tsv') # refused-only manifests preserve the game's offline registration path too PROG.set(step=', '.join(mods) + (f' and {len(nros)} NROs' if nros else ''), done=0, total=len(names), unit='modules') @@ -916,6 +929,7 @@ def rewrite(py, repo, exe_dir, out, env, logs, g): for f in logf.values(): f.close() rmtree(kf); swap_in(tmp, out); kf.write_text(f'{key}\n') PROG.end(', '.join(mods) + (f' and {len(nros)} NROs' if nros else '')) + return g GLSLC_SECS = 30 # the longest capture run: Three Houses compiles its 9 shaders within 1.2 s of boot, and the run stops as soon as every file the profile names is there @@ -1770,7 +1784,7 @@ def main(args=None): + (f'; the game\'s languages: {" ".join(have)}' if have else '; the control data lists no languages (not checked)') + (' (DM_LANG=<code> in the run command changes it)' if mac else ' (DM_LANG=<code> in the app\'s Documents/env.txt changes it)')) patched = work / f'patched_{pkg_ver}' - rewrite(py, repo, game / 'ExeFS', patched, env, logs, g) + g = rewrite(py, repo, game / 'ExeFS', patched, env, logs, g, provisional) chain_header(py, repo, env, logs) # before any host is built if provisional or derived: # the keys that need the rewritten modules, then (provisional) the heap check (both only change keys the host is built with, not the rewrite or the shader steps) provisional_prep(py, repo, env, logs, g)