From caee62a853ade4a136cd51bf2eeeab1c60df211d Mon Sep 17 00:00:00 2001 From: seedds Date: Mon, 26 Oct 2026 01:20:58 +0800 Subject: [PATCH] Compact NVN buffer bindings and remove Wonder heap workaround --- dm/games/010015100B514000/README.md | 20 +++--- dm/games/010015100B514000/game.tsv | 2 +- dm/games/010015100B514000/patches.py | 37 ---------- dm/gfx/nvn/hle_nvn.c | 8 ++- dm/gfx/nvn/nvn_mtl.m | 12 ++-- dm/gfx/nvn/nvn_obj.h | 17 ++++- dm/gfx/tests/t_nvn.c | 100 ++++++++++++++++++++++++++- dm/gfx/tests/t_reorder_chain.m | 16 ++++- dm/gfx/tests/t_vbuf.m | 15 +++- docs/agents/symptoms.md | 3 +- docs/reference/memory.md | 18 ++--- docs/reference/rewriter.md | 4 +- 12 files changed, 172 insertions(+), 80 deletions(-) delete mode 100644 dm/games/010015100B514000/patches.py 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/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 02d34b8..fd04b69 100644 --- a/dm/gfx/nvn/nvn_mtl.m +++ b/dm/gfx/nvn/nvn_mtl.m @@ -5495,8 +5495,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 @@ -7214,8 +7214,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; @@ -8239,8 +8239,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; } 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/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_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_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 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 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 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/docs/agents/symptoms.md b/docs/agents/symptoms.md index 5fdcf5b..8b0865f 100644 --- a/docs/agents/symptoms.md +++ b/docs/agents/symptoms.md @@ -56,8 +56,7 @@ 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) diff --git a/docs/reference/memory.md b/docs/reference/memory.md index 2a0dca4..6c5d201 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 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 `.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 ` 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