From c76f679834c9993a8a110277997ae33b602b5007 Mon Sep 17 00:00:00 2001 From: seedds Date: Mon, 26 Oct 2026 10:01:20 +0800 Subject: [PATCH] Remove alpha_from_red: Super Mario Odyssey's layout captures need only the nvnTextureCompare fix. The game creates each capture with its per-format default mapping (an R8 one reads (R, 0, 0, 1)), rebuilds it on the same storage with the capture's own, (1, 1, 1, R), and replaces the registered descriptor only on a difference (main+0x982320); while the host's comparison ignored the mapping the captures kept alpha 1 (white coin cards, a dark box behind hints), which the 26-shader alpha_from_red list papered over. Checked on the Mac from the "Look Around" hint with a probe host on the comparison (a c3a0880^ host for the profile's chain dec5ee141a9f libraries): 40 mapping-only rebuilds in 11,700 frames (R8 captures 640x80 down to 48x57 to (1, 1, 1, R), a 320x180 RGBA one to (R, G, B, A)); ignoring the mapping again brings back the box (about 12,800 pixels from frame 4,200), the old list hid it by converting the 378x80 capture, and with the fix the list converted nothing and changed no pixel of frames 4,200-6,600. The comparison and the views have no platform branch, so the iPhone gets the same rebuilt mapping (not run on a device). With no profile left using it, the key, its host code (tex_alpha_red, afr_want, DM_NVN_ALPHA_RED) and its docs go; treg_drop_ar becomes treg_drop_im (the image-slot view it also drops stays). Odyssey's note has the A/B runs. Checked: the host builds for Odyssey, the iOS compile check (133 files), the tool tests (111, 14 skipped) --- dm/games/0100000000010000/README.md | 4 +-- dm/games/0100000000010000/game.tsv | 5 ---- dm/games/01007EF00011E000/README.md | 4 +-- dm/games/README.md | 3 +- dm/games/game.py | 3 -- dm/games/profile_guess.py | 2 +- dm/gfx/nvn/nvn_mtl.m | 43 +++-------------------------- docs/agents/04-graphics.md | 1 - docs/reference/rendering.md | 2 +- 9 files changed, 10 insertions(+), 57 deletions(-) diff --git a/dm/games/0100000000010000/README.md b/dm/games/0100000000010000/README.md index 2cce0a1..f34a07d 100644 --- a/dm/games/0100000000010000/README.md +++ b/dm/games/0100000000010000/README.md @@ -6,7 +6,6 @@ Update 1.4.1 (v393216) over the base game, SDK 7.3.2. Nintendo EPD's sead / agl | Setting | Where | Why | |---|---|---| -| `alpha_from_red` with the 26 layout pixel shaders (`main`'s BNSH at 0x132c000) | [`game.tsv`](game.tsv) | the HUD's coin counters came out as solid white cards; inert on the Mac since the `nvnTextureCompare` fix, kept until an iPhone run; see [Rendering](#rendering) | | `pad_layout full` | `game.tsv` | the 3D gamepad layout | | 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | from the NSP script's provisional profile | | Node area cut and 32 MB alignment of large regions | `dm/tools/sdk_patches.py`, `sdk` build `fcf93a41…` | registered for DRAGON QUEST XI S on the same build; Odyssey has no virtual address memory and never allocates an address region | @@ -20,7 +19,7 @@ No `patches.py`. `op_mode` stays docked: Odyssey only takes its motion moves in ## Rendering -- **HUD (layout captures).** The coin counters (coin, regional coin) and other layout panes take their alpha from single-channel render targets, as in Breath of the Wild; read literally they were solid white cards, which `alpha_from_red` fixed. Odyssey's layouts rebuild their captures through the same agl code (`main+0x982320` replaces the registered descriptor only if `nvnTextureCompare` reports a difference), so the comparison fix covers them too. **Current:** on the Mac smoke route to frame 20,100 the list converted no texture (the host before the fix converted its first, a 640×80 R8 target, at frame 4129), and the HUD at frames 17,300 and 17,600 is pixel-identical. The list stays until the change has been seen on an iPhone. +- **Resolved: hard-edged dark boxes behind the HUD's layout panes** (the coin counters' solid white cards, a dark box behind the "Look Around" hint; layout captures, as in Breath of the Wild). The game creates each capture with its per-format default mapping (an R8 capture reads (R, 0, 0, 1)), rebuilds it on the same storage with the capture's own mapping, (1, 1, 1, R), and replaces the registered descriptor only if `nvnTextureCompare` reports a difference (`main+0x982320`). The host compared everything but the mapping, so the captures kept alpha 1. Fixed by the general [`nvnTextureCompare` compares the channel mapping](../../../docs/reference/rendering.md#nvntexturecompare-compares-the-channel-mapping). The profile's earlier workaround, `alpha_from_red` (all 26 layout pixel shaders, `main`'s BNSH at 0x132c000, read such targets as (R, 0, 0, R)), is gone with its host code. Checked on the Mac from the "Look Around" hint with a probe host on the comparison: 40 rebuilds in the first 11,700 frames change only the mapping, R8 captures (640×80, 400×120, 390×80, 378×80, 300×40, 250×50, 128×128, 64×64, 48×57) from (R, 0, 0, 1) to (1, 1, 1, R) and a 320×180 RGBA one from (R, G, B, 1) to (R, G, B, A). Ignoring the mapping again brings back a dark box behind the hint (about 12,800 pixels from frame 4,200 on); with the old list the workaround converted the 378×80 capture and the frames matched the fixed host's; with the fix, the list converted nothing and changed nothing (the frames 4,200 to 6,600 equal to the pixel but for the hint video's corner). The comparison and the views run the same on the iPhone (no platform branch), so the rebuilt captures leave no (R, 0, 0, 1) target for the list to find there either. - **Resolved: the Cap Kingdom fog never parted around Mario (reported on an iPhone; fix checked on the Mac).** The fog's shading reads velocity fields that a small fluid simulation updates every frame: two 256×256 and two 128×128 RG16F targets, with a two-pass blur and a blended composite. Its only input is a point-sprite splat per mover, drawn with `alPointSpriteShader`: one vertex per mover, its size in the vertex data (Mario's main splat about 18 px, others 3–5 px) and a soft disc in the fragment shader (`clamp(1 - 4r², 0, 1)` × velocity). The vertex shader writes `gl_PointSize = min(size, c0[0x610])`, and the host had nothing at that offset of the driver segment, so every splat had size 0 and the fields stayed exactly zero (raw dumps with `DM_NVN_TEXDUMP` + `DM_NVN_SNAPRAW`). The host now writes the largest point size, 2047, at `c0 + 0x610` for the graphics stages. Writing the `SetPointSize` value instead (always 1 here) left one-pixel splats whose disc weight came out 0 (the fields held `-0.0`). After the fix the fields hold velocities up to ±44 and the fog clears around Mario. - **Resolved: the Cap Kingdom ground showed 32×32 blocks of a stale image on every fourth frame (Mac).** Presents 4k were brighter and blocky, 4k+1 and 4k+2 clean, 4k+3 partly blocked, repeating to the decimal across runs; only with the ground's material (`b6384a85afd3ef94`) drawn. In some frames the game first draws a depth pre-pass (`alRenderDepthOnly`) for objects its occlusion queries report visible (the queries rotate through three slots, hence the cycle), then the G-buffer with other vertex shaders and depth test less-or-equal, without clearing the colour targets. Metal compiled the two vertex shaders' identical position math to slightly different depths, the G-buffer fragments failed the test and kept the previous content. Every Metal compile now uses `-fpreserve-invariance`. Ruled out on the way: the TBDR optimizations, same-pass feedback, the present path; evaluating the game's conditional rendering on the CPU from earlier frames' results made it worse (the condition belongs to the same frame on the GPU), so the host still ignores the condition here and draws (no `render_cond`). - Its three geometry shaders (a thunder effect) do not translate. @@ -63,7 +62,6 @@ General host changes first needed here; each applies to every game: ## Open -- `alpha_from_red` is inert on the Mac but stays until an iPhone run shows the HUD without it. - The three thunder geometry shaders do not translate. - The silent first minute of sound. - development.md calls SDK 7.3.2 "the oldest so far"; Mario Kart 8 Deluxe (SDK 4.4.0) is older now. diff --git a/dm/games/0100000000010000/game.tsv b/dm/games/0100000000010000/game.tsv index 966834e..842746a 100644 --- a/dm/games/0100000000010000/game.tsv +++ b/dm/games/0100000000010000/game.tsv @@ -27,11 +27,6 @@ mem_alias 0x10000000 mem_stack 0x10000000 app_pool 0xCD500000 -# ---- Layout captures ---- -# The HUD's counters (coin, regional coin) and other layout panes sample single-channel render targets mapped (R, 0, 0, 1) and use their alpha as the mask, as in Breath of the Wild: -# read literally the panes came out as solid white cards. All 26 pixel-shader variants of the layout library (main's BNSH at 0x132c000) -alpha_from_red 10bc9dd3825aec43 b9f0ca8022fbf9c2 4a7a63babf7a7f26 8d62cf444ba464bb 45df8d0a9963f552 71c48f95aa237856 ea95d9d8187d7500 db795e562f6ff4a2 88f7690ba13f9bca f6d7326dffcb030e 630cf891f10737f7 5cbeb3b0dc0fb242 323f44906717b824 c3aa5fd0d7b4cf8b ef78a74dcd99c62c 4081e659d6378648 233213b9bd8c8f5d e61e0be9f8906d53 c94174a0313de58d 2e56d8a39003cb57 8e768f35a7d9e589 7466b0ff0fc92d6e 04f46e974ed7af5c cb67cdd49f92c73c b5e04646ba8552ca 261b0556223fd2c1 - # ---- Shader split ---- shader_groups /ObjectData/=objectdata /StageData/=stagedata /SoundData/=sounddata /LayoutData/=layoutdata /LocalizedData/=localizeddata /EventData/=eventdata /MovieData/=moviedata /SystemData/=systemdata /ShaderData/=shaderdata /EffectData/=effectdata shader_note Super Mario Odyssey shaders diff --git a/dm/games/01007EF00011E000/README.md b/dm/games/01007EF00011E000/README.md index e832d77..5946b61 100644 --- a/dm/games/01007EF00011E000/README.md +++ b/dm/games/01007EF00011E000/README.md @@ -15,7 +15,7 @@ Update 1.9.0 (v1114112) over the base game, SDK 20.5.22. Nintendo EPD's engine ( | 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | no system resource in the NPDM: the game uses `SetHeapSize`. 13 Sentinels' partition (256 MB in front + 256 MB alias + 256 MB stack + 3.25 GB heap) | | Node area cut 1 GB → 62 MB | `dm/tools/sdk_patches.py`, `sdk` build `574284b4…` | registered for Tears of the Kingdom's issue #70 on the same `sdk` build, so it applies here too | -No `alpha_from_red` (no longer needed since the `nvnTextureCompare` fix), no `tex_flush_upload` (a minute's boot saw it flush 32 CPU-filled textures with data, and it needs none), no `feedback_snap`. +No `tex_flush_upload` (a minute's boot saw it flush 32 CPU-filled textures with data, and it needs none), no `feedback_snap`. ## Frame rate @@ -72,7 +72,7 @@ Used at `DM_FPS_TARGET=0` and for the interval the framework keeps while the VFR - **Cube textures sampled as cube arrays**: 1,512 shaders sample the environment probes as `texturecube_array` with a single cube bound; NVN cube textures are now 6-layer 2D arrays bound through views (`tex_fit`). - **Resolved: a compressed texture as a render target (GitHub issue #4, iPhone 14 Pro).** agl's BC4 block encoder (`8e2cc2671cae6020`) writes a 32×32 RGTC1 (BC4) texture through an `RGBA16UI` view (one texel per 8-byte block). Metal can neither attach a compressed format nor view one uncompressed, and pipeline validation aborted, so the draws did nothing. The pass now draws into an uncompressed shadow of blocks × blocks (`bcrt_attach` / `bcrt_flush`, from PR #11); its output is declared `uint` in `shader_facts_manual.tsv`, as is its 2D-source twin `93aa5e22a8202409`, whose failure showed up in an iPhone run. The A16 has no BC formats; the old decompression path (about 3.9 GB instead of 2.8 GB for this game, over iOS's limit on a 6 GB iPhone) has been removed. - **Resolved: the sky brownish grey, the distant haze brown (GitHub issue #52, Mac and iPhone).** Each frame a pass (`08fe25061d583637`, `uking_pass_shader.sharcb`) turns a 256×32×16 RGBA16F 3D table of in-scattered light into a 256×256 sky map, which the sky draw (`8110c74078548172`) and the materials' aerial perspective sample. The table and two 2D ones (transmittance 256×64, irradiance 64×64) come from `System/KSys/sky.skybin` in `Pack/Bootup_Graphics.pack` (1,212,416 bytes). The game makes a `CPU_CACHED | GPU_CACHED` pool per texture, creates the textures, copies the data in with `memcpy` and flushes (`main+0x1257d50`, once; the flush goes through `main+0xebf4c4`). The host made the 3D table a GPU texture of zeros. `DM_NVN_TEXRAW=212,214` and `DM_NVN_TEXINFO=256x32` (`storage 1048576 bytes non-zero 781567`, `swizzled=0`) showed it. Every game now fills a texture from storage the game flushed before its first use (`nvn_flushed`, `tex_init_fill`; `DM_NVN_FLUSH_FILL=0` turns it off). In the Great Plateau save it fills that one texture; the flushes (about 15 a frame) merge into about 530 ranges, frame times unchanged. -- **Resolved: dark boxes and cream cloud rectangles behind HUD prompts, a black stamina wheel and a black System menu background (layout captures, GitHub PR #6).** Panes tagged `CaptureOn` (about 140 layouts, e.g. `PaGuideKeySmoke_00`; user data `A8`, `CaptureScale` 0.5, `MultiFilter`) are drawn, blurred at half size and copied into a capture texture that replaces the placeholder `FBLayout_00`. The game creates it with its per-format default mapping (`main+0xecb0bc`: R8 → (R, 0, 0, 1), RG8 → (R, G, 0, 1), …), then rebuilds it on the same storage with the capture's mapping: `main+0xf597dc` picks (1, 1, 1, R) for an `A8` capture and alpha 1 when the pane says `CaptureOutputAlpha` 255 (read at `main+0xf58e98`); `main+0xecacf0` stores the mapping and rebuilds (`main+0xecabe8`), and `main+0xec33f0` replaces the registered descriptor only if `nvnTextureCompare` reports a difference. The host ignored the mapping, so the captures kept (R, 0, 0, 1): hard-edged dark boxes (the shadow pane `e6a88e12dbae9743`) and cream cloud (the glow `aa209f57b133e862`). The earlier `alpha_from_red` workaround (all 26 layout pixel shaders, `main`'s BNSH at 0x1d83000, `tex_alpha_red`) fixed the alpha but zeroed green and blue (a black stamina wheel) and left the System menu black. `nvnTextureCompare` now compares the mapping (an unset mapping counting as (R, G, B, A)); checked on the Mac by the PR's author: green stamina, the purple noise meter, the blurred System menu. +- **Resolved: dark boxes and cream cloud rectangles behind HUD prompts, a black stamina wheel and a black System menu background (layout captures, GitHub PR #6).** Panes tagged `CaptureOn` (about 140 layouts, e.g. `PaGuideKeySmoke_00`; user data `A8`, `CaptureScale` 0.5, `MultiFilter`) are drawn, blurred at half size and copied into a capture texture that replaces the placeholder `FBLayout_00`. The game creates it with its per-format default mapping (`main+0xecb0bc`: R8 → (R, 0, 0, 1), RG8 → (R, G, 0, 1), …), then rebuilds it on the same storage with the capture's mapping: `main+0xf597dc` picks (1, 1, 1, R) for an `A8` capture and alpha 1 when the pane says `CaptureOutputAlpha` 255 (read at `main+0xf58e98`); `main+0xecacf0` stores the mapping and rebuilds (`main+0xecabe8`), and `main+0xec33f0` replaces the registered descriptor only if `nvnTextureCompare` reports a difference. The host ignored the mapping, so the captures kept (R, 0, 0, 1): hard-edged dark boxes (the shadow pane `e6a88e12dbae9743`) and cream cloud (the glow `aa209f57b133e862`). The earlier `alpha_from_red` workaround (all 26 layout pixel shaders, `main`'s BNSH at 0x1d83000; since removed from the host) fixed the alpha but zeroed green and blue (a black stamina wheel) and left the System menu black. `nvnTextureCompare` now compares the mapping (an unset mapping counting as (R, G, B, A)); checked on the Mac by the PR's author: green stamina, the purple noise meter, the blurred System menu. - The game keeps NVN's GL depth mode. It has an `RG8_UI2F` vertex attribute (converted on the CPU, `vconv_i2f`). 101 compute and 4 vertex shaders read the driver segment `c0`; they render the same since compute `c0` carries the dispatch size. - It finalizes its transient render targets before every submit and builds them again the next frame, which the storage-takeover rule guards with a 16-submission window (`alias_take_over`). - Windowed on the Mac with `present_now`'s residency commit: its title, 60 fps, no GPU error. diff --git a/dm/games/README.md b/dm/games/README.md index 26f7a54..8e6a24e 100644 --- a/dm/games/README.md +++ b/dm/games/README.md @@ -139,7 +139,6 @@ One row per profile; the notes are each profile's `README.md`. | `perf_mode` | `follow` (the default): the performance mode follows the operation mode (Boost docked, Normal handheld); `boost`: Boost whatever the operation mode (`hle_oe.c`), a combination a Switch 1 never reports, so a profile choice. Rhythm Heaven Groove plays handheld with Boost: docked means a TV to the game, which then asks for a latency calibration, and only Boost gives its 1920x1080 draw size (handheld Normal draws 1280x720) | | `pad_layout` | The iPhone virtual gamepad's layout on first launch: `simple` (the default) = 2D (left stick, A/B/X/Y, shoulders, − / +), `full` = 3D (adds a D-pad, a right stick and L3 / R3; the right stick is not drawn: a touch landing in the right third of the screen on no control holds it, centred where it lands), `touch` = Tap (no control but the layout button: every touch goes to the game's touch screen; needs `op_mode handheld`, Balatro). The layout button at the top centre goes to the next layout in game (Tap only in handheld mode) and the app remembers the player's choice (`DmGamepad.m`) | | `feedback_snap` | Fragment shader keys (prefixes) whose draws sample a colour attachment of their own render pass and must read a snapshot instead (`fb_prepare` in `nvn_mtl.m`). Writing and sampling one attachment in a render pass is undefined in Metal: the Mac usually happens to read the old content, the iPhone sometimes reads whole 16×16 blocks as magenta. Each such draw costs a pass split and a copy, so only the shaders seen to break are listed (Super Mario 3D World + Bowser's Fury: its final composite; Captain Toad: Treasure Tracker: the same agl program, whose blocks showed on the Mac too). `DM_NVN_FB_SNAP=0` / `all` / `` overrides, also from `Documents/env.txt` | -| `alpha_from_red` | Fragment shader keys (prefixes, up to 48) that read a single-channel render target whose channel mapping is exactly (R, 0, 0, 1) as (R, 0, 0, R) (`tex_alpha_red` in `nvn_mtl.m`). It was a workaround for NintendoWare layout captures: the game rebuilds each capture with its own mapping ((1, 1, 1, R) for one tagged `A8`), and the textures kept (R, 0, 0, 1) only while the host's `nvnTextureCompare` ignored the mapping (fixed, see [04](../../docs/agents/04-graphics.md)). Breath of the Wild no longer lists it; Super Mario Odyssey still lists its 26 layout shaders. `DM_NVN_ALPHA_RED=0` / `` overrides, also from `Documents/env.txt` | | `tex_flush_upload` | `on` / `off` (the default): a texture the game fills from the CPU in place (no packaged layout, no `WriteTexels`) and publishes with `nvnMemoryPoolFlushMappedRange` is uploaded from guest memory like a packaged one, and re-uploaded when its pages change (`ft_add` in `hle_nvn.c`). Tears of the Kingdom streams all its textures this way; without it the 3D scene is black. Off by default because a flushed range in another game could cover an image a shader writes, whose GPU content the upload would replace. Without the key, every game still gets a texture's storage once when the game flushed it before the texture's first use (`nvn_flushed` in `hle_nvn.c`: Breath of the Wild's sky tables); the key is for textures the CPU writes again after that | | `render_cond` | `on` / `off` (the default): `SetRenderEnableConditional` / `SetRenderEnable` are recorded and evaluated at replay, and a timestamp report's counter field is 0 as on the GPU (`OP_RENDER_COND` in `nvn_mtl.m`). Tears of the Kingdom wraps its clears in conditions that skip clearing a target nothing has drawn into since its last clear; without the key every clear runs and its 3D scene is black. Animal Crossing: New Horizons too: four clear-only passes wiped its composite before the window copy, and every frame was black. Off by default: other games draw everything, as before | | `tex_dirty_upload` | `on` / `off` (the default), needs `tex_flush_upload`: a registered texture whose pages the CPU writes after `nvnTextureInitialize` without any NVN flush is uploaded as well (its pages are write-protected at registration, checked on first use; `nvn_ft_written` in `hle_nvn.c`). Tears of the Kingdom fills its ASTC terrain material arrays this way; without it the cave is magenta. FINAL FANTASY TACTICS - The Ivalice Chronicles loads its UI textures straight into CPU-uncached pools, which need no flush, after creating them; without both keys its screen stays black. The Great Ace Attorney Chronicles copies every texture into a CPU-cached pool after creating it and never flushes (its `main` has no cache maintenance at all): black too | @@ -175,7 +174,7 @@ These are not profile keys: the tools read them from the game files each time, b After the rewrite, `mem_used` is set when the rewritten modules exceed the default 64 MB (a game without a system resource); keep it when the profile moves into `dm/games` (Cuphead without it: a 0.03 GB heap and an abort at the first allocation). A game with a system resource that sets a heap then gets a **heap check**: it boots in the Mac host (headless, no shader libraries) until its first heap request, or 5 s into `nnMain` without one, at most 30 s (`HEAP_CHECK_SECS`). A request the heap region cannot hold (`SetHeapSize OOM`) sets the alias / heap split (Tears of the Kingdom asks for 0x86000000, which gives its hand-made 0x98000000 alias region; Wonder's 1.44 GB gives 0xc0000000), and a pool check that fails for good sets `mem_used`. A run with no heap set whose alias region is used up to within 0.25 GB of its end sets a 1 GB pool: `nninitStartup` handed pool − used to StandardAllocator as one address region and the game reserves regions of its own beside it (Unity 2020 and 2022: AI: THE SOMNIUM FILES - nirvanA Initiative reached +4.26 GB of 4.38 in 5 s with the hardware pool; Unity 2018.3's Raging Loop maps only inside StandardAllocator's region and keeps it). The 1 GB is a starting point, not a measurement: Advance Wars 1+2: Re-Boot Camp (Unity 2020.2) ran its malloc dry at its first loading screen with it and needs 2 GB. The profile gets a `# heap check:` line, and later runs keep its keys (delete the line to check again; `--no-heap-check` / `HEAP_CHECK = False` skips it). Other games skip the check, so it costs nothing for them and a few seconds plus a Mac host build for the rest. - No short run decides `render_cond`, `tex_flush_upload`, `tex_dirty_upload`, `core_preempt`, `gpu_timestamps`, `feedback_snap`, `alpha_from_red`, `vconv_range` or `present_interval`, nor `op_mode` / `pad_layout` outside the presets: they keep their defaults for a developer. A minute's boot without input counted Wonder's clears under render conditions (65,536 or more; it needs no `render_cond`) as readily as Tears of the Kingdom's, saw Breath of the Wild flush 32 CPU-filled textures with data (it needs no `tex_flush_upload`) and Age of Calamity none (its movie frames come later), and the old host's Metroid Dread movie crash showed in about half the runs (now resolved by [shared wake handoffs](../../docs/reference/threads.md#same-core-wake-handoffs)). On the iPhone, `Documents/env.txt` can still set `DM_OPMODE`, `DM_NVN_GPU_TS` and `DM_NVN_FB_SNAP`, and the pad layout switches in game. + No short run decides `render_cond`, `tex_flush_upload`, `tex_dirty_upload`, `core_preempt`, `gpu_timestamps`, `feedback_snap`, `vconv_range` or `present_interval`, nor `op_mode` / `pad_layout` outside the presets: they keep their defaults for a developer. A minute's boot without input counted Wonder's clears under render conditions (65,536 or more; it needs no `render_cond`) as readily as Tears of the Kingdom's, saw Breath of the Wild flush 32 CPU-filled textures with data (it needs no `tex_flush_upload`) and Age of Calamity none (its movie frames come later), and the old host's Metroid Dread movie crash showed in about half the runs (now resolved by [shared wake handoffs](../../docs/reference/threads.md#same-core-wake-handoffs)). On the iPhone, `Documents/env.txt` can still set `DM_OPMODE`, `DM_NVN_GPU_TS` and `DM_NVN_FB_SNAP`, and the pad layout switches in game. The shader tables, `c1_full.tsv` and `cb_extent.tsv` are written into the same folder. A second run reuses that `game.tsv` (delete it to survey again), so it can be edited by hand. `--provisional` builds from a provisional profile even for a game that has one in `dm/games`, to test the guesses: `DM_GAMES_FIRST=1` makes every tool search `DM_GAMES_DIR` first, and the IPA gets `-provisional` in its name. Tools find profiles outside `dm/games` through `DM_GAMES_DIR` (folders separated by `:`, searched after `dm/games`); `make -C dm/host` reaches such a profile through the symlink `dm/host/gen//profile`. To keep a game, move the folder into `dm/games/` and bring it up as below. 2. **Profile.** Copy `010015100B514000/game.tsv` (or the provisional profile) to `dm/games//game.tsv` (the base game's title ID, as the folder name) and fill in the identity, `build` rows and `modules`. Leave the memory values as Mario's until the bring-up measures them ([docs/agents](../../docs/agents/README.md)). diff --git a/dm/games/game.py b/dm/games/game.py index fdabd14..fc99a2f 100644 --- a/dm/games/game.py +++ b/dm/games/game.py @@ -113,8 +113,6 @@ def header(g): if pl not in ('simple', 'full', 'touch'): sys.exit(f'{g.id}: pad_layout must be simple, full or touch: {pl}') # touch = Tap (no control: every touch goes to the game's touch screen) fbs = g.words('feedback_snap', '') # fragment shader key prefixes whose same-pass feedback sampling gets a snapshot first (nvn_mtl.m fb_prepare); none by default if any(not re.fullmatch(r'[0-9a-f]{8,16}', k) for k in fbs): sys.exit(f'{g.id}: feedback_snap takes fragment shader keys (8-16 lowercase hex digits): {fbs}') - afr = g.words('alpha_from_red', '') # fragment shader key prefixes that read single-channel textures mapped (R, 0, 0, 1) with alpha = red (nvn_mtl.m tex_alpha_red); none by default - if any(not re.fullmatch(r'[0-9a-f]{8,16}', k) for k in afr): sys.exit(f'{g.id}: alpha_from_red takes fragment shader keys (8-16 lowercase hex digits): {afr}') rcd = g.get('render_cond', 'off') # NVN conditional rendering evaluated at replay, timestamp reports with counter 0 (nvn_mtl.m OP_RENDER_COND): on / off if rcd not in ('on', 'off'): sys.exit(f'{g.id}: render_cond must be on or off: {rcd}') tfu = g.get('tex_flush_upload', 'off') # textures written by the CPU into a pool and published with nvnMemoryPoolFlushMappedRange are uploaded like packaged ones (hle_nvn.c): on / off @@ -167,7 +165,6 @@ def header(g): f'#define DM_GAME_OPMODE {int(om == "console")}', f'#define DM_GAME_PERF_BOOST {int(pm == "boost")}', f'#define DM_GAME_FEEDBACK_SNAP {q(" ".join(fbs))}', - f'#define DM_GAME_ALPHA_FROM_RED {q(" ".join(afr))}', f'#define DM_GAME_TEX_FLUSH_UPLOAD {int(tfu == "on")}', f'#define DM_GAME_RENDER_COND {int(rcd == "on")}', f'#define DM_GAME_TEX_DIRTY_UPLOAD {int(tdu == "on")}', diff --git a/dm/games/profile_guess.py b/dm/games/profile_guess.py index faac15d..33f4686 100644 --- a/dm/games/profile_guess.py +++ b/dm/games/profile_guess.py @@ -4,7 +4,7 @@ # guess(facts) turns the facts into profile keys, each with the reason written above it in game.tsv. Every rule was checked against the hand-made profiles; --check runs that comparison again. # mem_used needs the rewritten modules (mem_used below, after the rewrite). The alias / heap split of a game with virtual address memory that sets a heap needs its heap request: # the NSP script's heap check boots it on the Mac until that request (heap_signals below), a few seconds. The keys no short run decides (render_cond, tex_flush_upload, -# tex_dirty_upload, core_preempt, gpu_timestamps, feedback_snap, alpha_from_red, vconv_range, present_interval) keep their defaults for a developer: a minute's boot without input +# tex_dirty_upload, core_preempt, gpu_timestamps, feedback_snap, vconv_range, present_interval) keep their defaults for a developer: a minute's boot without input # cannot tell the games that need them (Tears of the Kingdom) from those that only use what they answer (Wonder's conditional clears, Breath of the Wild's flushed textures). # Usage: profile_guess.py <ExeFS dir> <romfs_manifest.tsv | RomFS dir> the facts and the guessed keys # profile_guess.py --check [<NSP root>] every profile in dm/games against its extraction under <NSP root> (default /Volumes/T7/nsp): where the guess differs diff --git a/dm/gfx/nvn/nvn_mtl.m b/dm/gfx/nvn/nvn_mtl.m index f040d83..0fc551c 100644 --- a/dm/gfx/nvn/nvn_mtl.m +++ b/dm/gfx/nvn/nvn_mtl.m @@ -3013,7 +3013,7 @@ static void shmap_load(void) { } @interface NvShader : NSObject { @public id<MTLFunction> fn, tf_fn, fn_u, fn_m, geo_fn; uint8_t mask_m; // fn_u: the integer-output entry point (dmsh_build.py fs_uint), for pipelines whose colour attachments are all unsigned integer (pso_desc); fn_m: the mixed one, its outputs in mask_m unsigned integer - uint16_t tf_stride[4]; uint32_t cb_ext[16], sb_ro; nvn_refl rf; id<MTLArgumentEncoder> ae; NSUInteger arg_len; int stage, range_ro, afr, layer_out; const char *builtin; uint64_t logged; char key[24]; id<MTLBuffer> c1buf; uint64_t c1_gpu; size_t c1off; + uint16_t tf_stride[4]; uint32_t cb_ext[16], sb_ro; nvn_refl rf; id<MTLArgumentEncoder> ae; NSUInteger arg_len; int stage, range_ro, layer_out; const char *builtin; uint64_t logged; char key[24]; id<MTLBuffer> c1buf; uint64_t c1_gpu; size_t c1off; uint32_t geo_vertices, geo_invocations, geo_output, geo_stride, geo_index_stride; id<MTLComputePipelineState> geo_pso; int gs_loc, lay_loc; char gs_comp, lay_comp; // a slice-writing geometry shader (no function: the layer comes from input gs_loc.gs_comp) / a vertex shader with layer entry points writing output lay_loc.lay_comp (dmsh_build.py gs_slice, vs_layer_entry); -1 = none @@ -3372,7 +3372,6 @@ void mtl_prog_release(prog_t *p) { if (p->serial) serial_del(p->serial); } // At registration the whole texture object is snapshotted (NVN semantics: the descriptor has been written into the pool); view: the MTLTexture for that ID (a view or the base texture, held via CFBridgingRetain) and the serial-table / guest-write generations at the time it was obtained typedef struct { tex_t t; int ok; view_t v; int has_v; void *view; uint64_t view_id, view_rid, view_ser, view_dirty; int view_pkg; // view_pkg: CPU-written (packaged); only those depend on guest writes (view_dirty) void *alt, *alt_of; uint64_t alt_rid; uint8_t alt_type; // alt: compatible view used when the shader's declared type differs from view (tex_fit), alt_of = the view it was built from - void *ar, *ar_of; uint64_t ar_rid; // ar: the alpha-from-red view for the profile's alpha_from_red shaders (tex_alpha_red), ar_of = the view it was built from void *im, *im_of; uint64_t im_rid; } treg_t; // im: the view an image slot reads in the shader's pixel class (img_fit), im_of = the view it was built from static treg_t *g_treg; static int g_ntreg; // Lock-free copy of texture ID → serial (the replay hot path only needs the serial: store lookahead index, binding set of the clear-only hoist): texture IDs are 20 bits, fixed 1M entries (BSS, only touched pages are used), @@ -3381,8 +3380,7 @@ static _Atomic uint64_t g_tser[1 << 20]; static samp_par *g_sreg; static uint8_t *g_sreg_ok; static void **g_sview; static uint64_t *g_srid; static int g_nsreg; // g_sview: sampler ID → MTLSamplerState (retained) // Memory pool Finalize: for texture IDs whose registered texture lives on this pool, drop the cached MTLTexture (view or base texture). The cache used to be released only when the ID was registered again, // so a linear texture built directly on a pool kept the whole pool buffer alive (6 to 17 finalized 16 MB pool buffers stayed alive in Engage's title demo). If the ID is still used, the next lookup rebuilds it on the slow path -static void treg_drop_ar(treg_t *r) { if (r->ar) { defer_obj_unref((__bridge id)r->ar); CFRelease(r->ar); r->ar = r->ar_of = NULL; r->ar_rid = 0; } - if (r->im) { defer_obj_unref((__bridge id)r->im); CFRelease(r->im); r->im = r->im_of = NULL; r->im_rid = 0; } } // and the image class view (img_fit) +static void treg_drop_im(treg_t *r) { if (r->im) { defer_obj_unref((__bridge id)r->im); CFRelease(r->im); r->im = r->im_of = NULL; r->im_rid = 0; } } // the image class view (img_fit) // Texture views by texture serial and view parameters, beside the per-ID view: KTGL (Age of Calamity) registers a texture into the next ID of a ring before every bind (about 21,000 // registrations a frame in battle after the first cutscene, 5 in 6 of them a different texture than the ID held), so the per-ID view was rebuilt for nearly every bind: half the // registrations ended in a new Metal texture view, 45% of the replay's time. A hit is checked against the current base texture (re-uploads and format-view upgrades replace it). @@ -3472,7 +3470,7 @@ static void treg_drop_pool_views(const void *pool) { 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]); } + 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_im(&g_treg[i]); } os_unfair_lock_unlock(&g_rmu); } void mtl_reg_tex(int tid, const tex_t *t, const view_t *v) { @@ -3487,7 +3485,7 @@ void mtl_reg_tex(int tid, const tex_t *t, const view_t *v) { g_reg_gen++; if (rr->view) { defer_obj_unref((__bridge id)rr->view); CFRelease(rr->view); rr->view = NULL; rr->view_id = rr->view_rid = 0; } if (rr->alt) { defer_obj_unref((__bridge id)rr->alt); CFRelease(rr->alt); rr->alt = rr->alt_of = NULL; rr->alt_rid = 0; } - treg_drop_ar(rr); + treg_drop_im(rr); 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); } @@ -3677,38 +3675,6 @@ static void tex_mis_note(NvShader *sh, int stage, int slot, int tid, id<MTLTextu MLOG("[RED] texture type mismatch: shader %s (stage %d) declares slot %d as Metal type %lu, texture ID %d is type %lu (NVN target %d, format %#x, Metal format %lu%s); %s", sh->key, stage, slot, (unsigned long)want, tid, (unsigned long)t.textureType, tt ? (int)tt->d.target : -1, tt ? (unsigned)tt->d.format : 0u, (unsigned long)t.pixelFormat, bt, outcome); } -// Alpha from red: the fragment shaders the game profile's alpha_from_red lists read a single-channel render target whose channel mapping is exactly (R, 0, 0, 1) as (R, 0, 0, R). -// A workaround for NintendoWare layout captures (BotW, Odyssey): the game rebuilds each capture on the same storage with its own mapping ((1, 1, 1, R) for an A8 capture), which was lost while -// nvnTextureCompare ignored the mapping and left the creation-time (R, 0, 0, 1), alpha always 1 (hard-edged prompt boxes, white coin cards). Since that fix it no longer fires (docs/reference/rendering.md, "`nvnTextureCompare` compares the channel mapping"). -// Only render targets qualify (captures always are; asset and CPU-written textures are not). DM_NVN_ALPHA_RED=0 turns it off, <keys,...> replaces the list. Caller holds g_rmu -static char g_afr_keys[48][20]; static int g_afr_nk = -1; -static int afr_want(NvShader *sh) { - if (!sh->afr) { - if (g_afr_nk < 0) { g_afr_nk = 0; const char *e = getenv("DM_NVN_ALPHA_RED"), *src = e && *e ? e : DM_GAME_ALPHA_FROM_RED; - if (strcmp(src, "0")) for (const char *q = src; *q && g_afr_nk < 48;) { size_t n = strcspn(q, ", "); if (n) { if (n > 16) n = 16; memcpy(g_afr_keys[g_afr_nk], q, n); g_afr_keys[g_afr_nk++][n] = 0; } q += n; q += strspn(q, ", "); } - if (g_afr_nk) MLOG("single-channel texture alpha from red: %s%s", src, e && *e ? " (DM_NVN_ALPHA_RED)" : " (profile key alpha_from_red)"); } - sh->afr = 1; for (int i = 0; i < g_afr_nk; i++) if (!strncmp(sh->key, g_afr_keys[i], strlen(g_afr_keys[i]))) sh->afr = 2; - } - return sh->afr == 2; -} -static id<MTLTexture> tex_alpha_red(int tid, id<MTLTexture> t, uint64_t *rid) { - MTLPixelFormat pf = t.pixelFormat; MTLTextureSwizzleChannels sw = t.swizzle; - if (pf != MTLPixelFormatR8Unorm && pf != MTLPixelFormatR16Unorm && pf != MTLPixelFormatR16Float && pf != MTLPixelFormatR32Float) return nil; - if (sw.red != MTLTextureSwizzleRed || sw.green != MTLTextureSwizzleZero || sw.blue != MTLTextureSwizzleZero || sw.alpha != MTLTextureSwizzleOne || tid <= 0 || tid >= g_ntreg) return nil; - id<MTLTexture> base = t.parentTexture ? t.parentTexture : t; - if (!(base.usage & MTLTextureUsageRenderTarget)) return nil; // packaged and CPU-written textures are built without render-target usage - treg_t *r = &g_treg[tid]; - if (r->ar && r->ar_of == (__bridge void *)t) { *rid = r->ar_rid; return (__bridge id<MTLTexture>)r->ar; } - treg_drop_ar(r); - NSUInteger l0 = t.parentTexture ? t.parentRelativeLevel : 0, s0 = t.parentTexture ? t.parentRelativeSlice : 0; - NSUInteger ns = t.textureType == MTLTextureType3D ? 1 : t.arrayLength * (t.textureType == MTLTextureTypeCube || t.textureType == MTLTextureTypeCubeArray ? 6 : 1); - id<MTLTexture> v = [base newTextureViewWithPixelFormat:pf textureType:t.textureType levels:NSMakeRange(l0, t.mipmapLevelCount) slices:NSMakeRange(s0, ns) - swizzle:MTLTextureSwizzleChannelsMake(MTLTextureSwizzleRed, MTLTextureSwizzleZero, MTLTextureSwizzleZero, MTLTextureSwizzleRed)]; - if (!v) { RED_ONCE(82, "cannot create the alpha-from-red view (texture ID %d format %lu)", tid, (unsigned long)pf); return nil; } - { static int once; if (!once++) MLOG("single-channel texture alpha from red: first case texture ID %d %lux%lu", tid, (unsigned long)t.width, (unsigned long)t.height); } - r->ar = (void *)CFBridgingRetain(v); r->ar_of = (__bridge void *)t; r->ar_rid = v.gpuResourceID._impl; - *rid = r->ar_rid; return v; -} // Pixel class of a Metal format as a shader's texture type sees it (NVN_DPF_*): the Uint / Sint formats, everything else reads as float static int mtl_pf_dpf(MTLPixelFormat pf) { switch ((int)pf) { @@ -5524,7 +5490,6 @@ static int encode_args_(id<MTLCommandEncoder> en, NvShader *sh, int nvn_stage, i // no view fits, and one side is a buffer texture (Octopath Traveler 0: a buffer texture left in a slot a shader reads as a cube): reads of it are undefined, so the slot reads 0 else if (t.textureType == MTLTextureTypeTextureBuffer || (MTLTextureType)b->dtt == MTLTextureTypeTextureBuffer) { tex_mis_note(sh, nvn_stage, b->kind == NVN_RB_TEX ? reg : -1, tid, t, (MTLTextureType)b->dtt, "a buffer texture: the slot reads 0 (dummy)"); t = nil; } else tex_mis_note(sh, nvn_stage, b->kind == NVN_RB_TEX ? reg : -1, tid, t, (MTLTextureType)b->dtt, "bound as is"); } - if (t && nvn_stage == 1 && afr_want(sh)) { uint64_t r2 = 0; id<MTLTexture> v = tex_alpha_red(tid, t, &r2); if (v) { t = v; trid = r2; } } // see tex_alpha_red if (!t) { t = dummy_tex(b->dtt, b->dpf); trid = g_dummy_tex_rid[b->dtt & 7][b->dpf & 3]; } slots[aid] = trid; USE(t, MTLResourceUsageRead); diff --git a/docs/agents/04-graphics.md b/docs/agents/04-graphics.md index 70fff4d..a8b8767 100644 --- a/docs/agents/04-graphics.md +++ b/docs/agents/04-graphics.md @@ -83,7 +83,6 @@ What earlier titles needed beyond sections 1–5 is filed by mechanism: textures | `DM_NVN_MAGSCAN=<fs key>[:every[:phase]]` | Device self-check for solid colour blocks. Every `every` frames (default 30) a compute kernel reads each render pass's colour attachments as the pass ends and logs `[mag]` lines for 16×16 blocks that hold one magenta value throughout, or non-finite pixels, naming the pass relative to the one holding that shader. Every `phase` frames (default 600) it rotates: 0 draws as usual, 1 drops that shader's draws, 2 / 3 keep its geometry with a constant grey / bright magenta fragment output. Each phase ends with a summary line, including the magenta pixel count in that shader's own pass | | `DM_NVN_PREDICT=0` | No parallel precompile prediction: a pipeline miss compiles only the pipeline it needs. For telling a fault of the look-ahead from one of the pipelines ([reference](../reference/shaders.md#the-precompile-look-ahead-restores-the-scissor)) | | `DM_NVN_FB_SNAP=0` / `all` / `<keys>` | Which fragment shaders get the snapshot for same-pass feedback sampling (a draw sampling a colour attachment of its own render pass reads a copy taken just before it): none, every shader (slow, see BotW above), or a comma-separated key list instead of the profile's `feedback_snap` | -| `DM_NVN_ALPHA_RED=0` / `<keys>` | Which fragment shaders read single-channel render targets mapped (R, 0, 0, 1) with alpha = red: none, or a comma-separated key list instead of the profile's `alpha_from_red` (Odyssey's layout shaders; BotW no longer needs it since the `nvnTextureCompare` fix, see above) | | `DM_NVN_CUT=<draws>` | Commit the replay's command buffer every that many draws, between passes (default 64; 0 = one command buffer per submission, except the cut at the first pass boundary after a buffer → texture copy, which always happens: Seen in Catherine: Full Body). Growing segments (64, 128, 256 … draws) were tried for fewer commits: Breath of the Wild fell from 60 to 30 fps in the same scene (GPU idle while the long last segments were encoded), and the commits cost little once the residency set stopped churning | | `DM_NVN_UB_DEFER=0` | End the render pass at every inline uniform update (`nvnCommandBufferUpdateUniformBuffer`), as before. By default an update inside an open pass is deferred to the pass end; the draws after it read a shadow made of the deferred updates when they cover all the shader can read of the binding (`cb_extent.tsv`), or of memory and the updates queued in the submission when they cover part of it (`DM_NVN_UB_PARTIAL`), and the pass still ends first for a draw that needs more (the data read as vertices, indices, indirect arguments or a storage buffer, or a partly covered block memory cannot give). The `residency set` line counts deferred updates, shadows read from memory and early ends | | `DM_NVN_UB_PARTIAL=0` | A uniform block the deferred updates cover only in part ends the pass, as before. By default its shadow starts from memory and takes every inline update queued in the submission, newest last, when nothing else can have written the range by the time the pass runs: no copy, fill or transform-feedback write queued in the submission, no storage buffer a shader may write (every one but the slots `cb_extent.tsv` proves unwritten) and no texture→buffer copy over it in the submission, and no GPU write or counter report of an earlier submission still running (by 4 KB page). A counter report of the same submission does not count: the host writes reports on the CPU after the whole submission completes. The `residency set` line splits the refusals by reason | diff --git a/docs/reference/rendering.md b/docs/reference/rendering.md index 4b48d0f..81eba8d 100644 --- a/docs/reference/rendering.md +++ b/docs/reference/rendering.md @@ -31,7 +31,7 @@ The replay on Metal (`dm/gfx/nvn/nvn_mtl.m`) and the Vulkan and OpenGL layers. E - **Cause:** agl's layouts rebuild a capture texture on the same storage with a new channel mapping, and replace the registered descriptor only if `nvnTextureCompare` reports the two textures different. The host compared the description and the storage but not the mapping, so every rebuild was dropped. - **Fix:** `nvnTextureCompare` compares the mapping too, an unset mapping counting as (R, G, B, A). `t_nvn` covers the comparison. From GitHub PR #6. - **Found on:** [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md): panes tagged `CaptureOn` (about 140 layouts, e.g. `PaGuideKeySmoke_00`; user data `A8`, `CaptureScale` 0.5, `MultiFilter`) are drawn into a target, blurred at half size and copied into a capture texture that the layout's materials sample in place of the placeholder `FBLayout_00`. The game creates it with its per-format default mapping (one call site, `main+0xecb0bc`: R8 → (R, 0, 0, 1), RG8 → (R, G, 0, 1), and so on), then rebuilds it with the capture's own mapping: `main+0xf597dc` picks (1, 1, 1, R) for an `A8` capture (the material colour kept, the mask as alpha) and alpha 1 when the pane says `CaptureOutputAlpha` 255 (read at `main+0xf58e98`; an RGB10A2 capture then reads (R, G, B, 1)); `main+0xecacf0` stores the mapping and rebuilds the texture (`main+0xecabe8`), and `main+0xec33f0` replaces the descriptor only on a difference. The captures kept (R, 0, 0, 1), whose alpha is always 1: hard-edged dark boxes behind every prompt (the shadow pane `e6a88e12dbae9743`: black, alpha = capture alpha × vertex alpha 180/255) and rectangles filled with cream cloud (the glow `aa209f57b133e862`: alpha = min(cloud alpha, capture alpha)). Checked on the Mac by the PR's author (M4 Pro, unchanged shader libraries): green stamina, the purple noise meter and the game's own blurred System menu background. [Super Mario Odyssey](../../dm/games/0100000000010000/README.md): its layouts rebuild their captures through the same agl code (`main+0x982320` replaces the registered descriptor only if `nvnTextureCompare` reports a difference), so the fix covers them too. -- **Note:** before the cause was known, the profile key `alpha_from_red` listed the layout library's pixel shaders and bound such render targets through a view mapped (R, 0, 0, R) (`tex_alpha_red` in `nvn_mtl.m`). In BotW (all 26 pixel-shader variants, `main`'s BNSH at 0x1d83000) that fixed the alpha but zeroed green and blue, so tinted panes such as the stamina wheel came out black, and the System menu background (a capture with alpha forced to 1) stayed black; BotW's profile no longer lists it. In Odyssey (all 26 layout pixel shaders, `main`'s BNSH at 0x132c000) it had turned the coin counters' solid white cards into the right shapes; with the fix it no longer fires: on the Mac smoke route to frame 20,100 it converted no texture (main's host converted its first, a 640×80 R8 target, at frame 4129), and the HUD at frames 17,300 and 17,600 is pixel-identical to main's. Odyssey's list stays until the change has been seen on an iPhone. +- **Note:** before the cause was known, a profile key, `alpha_from_red`, listed the layout library's pixel shaders and bound such render targets through a view mapped (R, 0, 0, R). In BotW (all 26 pixel-shader variants, `main`'s BNSH at 0x1d83000) that fixed the alpha but zeroed green and blue, so tinted panes such as the stamina wheel came out black, and the System menu background (a capture with alpha forced to 1) stayed black. In Odyssey (all 26 layout pixel shaders, `main`'s BNSH at 0x132c000) it had turned the coin counters' solid white cards into the right shapes. With the comparison fixed it found no (R, 0, 0, 1) target on either game (the mapping a capture is rebuilt with replaces it, on the Mac and the iPhone alike), so the key and its host code (`tex_alpha_red`, `DM_NVN_ALPHA_RED`) were removed; Odyssey's note has the A/B runs. ### Texture views cached by texture and view parameters