Skip to content

Fix RGBA4 channel order and Super Mario RPG battle lighting - #993

Open
MKLOL wants to merge 4 commits into
seedds:mainfrom
MKLOL:codex/nvn-rgba4-component-order
Open

MKLOL wants to merge 4 commits into
seedds:mainfrom
MKLOL:codex/nvn-rgba4-component-order

Conversation

@MKLOL

@MKLOL MKLOL commented Nov 2, 2026 •

Copy link
Copy Markdown

What and why

In Super Mario RPG 1.0.1, Mushroom Way's battle ground and lower foliage lose their sunlight while Mario and the UI stay lit. The baked shadowmask is an RGBA4 texture. NVN's RGBA4 holds R, G, B, A from bit 0 up; Metal's ABGR4Unorm holds A, B, G, R. The host kept the guest's bits, so every channel read another channel's nibble. The shadowmask's texels are mostly 0x0f00, sampled through a (B, G, R, A) mapping: red should read 1, and read 0 with green 1.

RGBA4 now goes through the conversion the RGB565 family already has (#183), reversing all four nibbles where that family exchanges red and blue:

  • nvn_fmt_to_mtl maps RGBA4 with reversed components.
  • rb_packed_shift returns 4 for it.
  • rb_swap_rows and rb_swap.metal reverse the nibbles for shift 4.
  • tex_by_id resets all four components, not just red and blue, for host textures that already hold Metal's order.

Uploads, initial fills, buffer ↔ texture copies, ReadTexels and storage readbacks convert at the same crossings as before. Linear and buffer textures, which read guest memory in place, take the reversed mapping when sampled. The change is general host behaviour, with no game-specific rule; the shader chain is unchanged.

Performance note: like the RGB565 family, RGBA4 uploads are now repacked on the CPU and no longer use the GPU deswizzle. The measured cost on Super Mario RPG is in "Checked".

The RGB565 family has a documented limit that applies here too: a same-size view in another format (an R16 view of an RGBA4 texture) sees Metal's order. No game is known to make one.

The PR also adds a smoke route for Super Mario RPG (smoke.tsv), so regress.py plays it. The route starts from a save on Mushroom Way and goes into the battle this fix lights.

Screenshots

The same copied Mushroom Way checkpoint, unedited native Metal snapshots. These were taken during the first round of this PR. The smoke route below reproduces the same pair: dark on main, lit with this change.

Before (unmodified 65501fbb):

Upstream: dark Mushroom Way battle

After:

RGBA4 fix: restored Mushroom Way sunlight

Checked

Apple M5 Max, macOS 27.0.1, Xcode 27.2.

  • GPU tests:
    • t_view_swizzle passes 491 checks with Metal API validation. New cases sample the RGBA4 texels 0x8421 and 0x0f00 (every nibble different): uploaded and on a buffer, with and without the (B, G, R, A) mapping, cleared by Metal, and filled from CPU_UNCACHED and CPU_NO_ACCESS pools. Another new case covers RGB565 read in place.
    • t_readback passes 148 / 149 cases (fast wake on / off). Its RGB565-family copy, clear and ReadTexels cases now also run for RGBA4.
    • On main's host code these tests fail: 40 of t_view_swizzle's 491 checks and 6 of t_readback's 149 cases. Case 425 reproduces the game's red 0 / green 1.
  • Other suites:
    • The full gfx suite (make -C dm/gfx/tests GAME=0100BC0018138000 run) passes, as do the CPU texture tests (5,344 / 5,344).
    • Tool tests: 906, 16 skipped, OK.
    • Locally, with CI's GAME: host unit tests, ASan/UBSan/TSan, CPU-only gfx tests, the Mac host for three profiles, every gfx test compiled, the iOS compile check (258 files) and the unsigned iOS app.
  • clang-tidy (LLVM 23.1.4): the same result as main. The only findings over the baseline are main's two in t_geometry_hazards.m, which Release the geometry fixture program and check readback allocation #958, Release registered Metal programs after each geometry-hazard test #959 and C lint: t_geometry_hazards checks its readback buffer, and notes who owns its program #979 fix, so this PR's lint check stays red until one of them lands.
  • Super Mario RPG, new smoke route (regress.py check, full mode, one run each, main 65501fbb against this change):
    • Frame rate in the battle menu: 59.0 → 59.8 fps, p50 16.50 → 16.56 ms, p99 35.96 → 24.88 ms.
    • Peak memory: 5000 → 4939 MB. Prepare: 0.26 → 0.25 s. No new red lines, no crash or hang.
    • Texture uploads over the whole run: CPU 79 → 77 ms, GPU 167 → 158 ms (the frame rows' @upload_ms / @upload_gpu_ms). So moving RGBA4 to the CPU costs nothing measurable here.
    • The harness marks the result REGRESSION because the battle screenshots at 55 s and 70 s differ (dHash 27 bits). That difference is the fix: main's arena is dark, this change's is lit. The field screenshot at 30 s is unchanged (0 bits).
  • 13 Sentinels: chain check only (it has no smoke route). The change only reaches RGBA4 textures.
  • Not checked: iOS on a device.
regress.py impact main..HEAD

The function-level impact selects the three games that have usage manifests. None of them is on this Mac, so they are unchecked. Whether they use RGBA4 textures at all is unknown: the change only acts on that format.

game status what it uses of the change
The Legend of Zelda: Breath of the Wild 01007EF00011E000 affected NVN submit / present / texture pool (the host's callers of rb_packed_shift, rb_swap_rows)
Pokémon Scarlet 0100A3D008C5C000 affected the same
The Legend of Zelda: Tears of the Kingdom 0100F2C0115B6000 affected the same, plus nvnTextureReadTexels ×25
Hades 0100535012974000 broad every game's host shaders (rb_swap.metal)

126 profiles have no usage manifest.

regress.py check (full) on Super Mario RPG with the new route

Baseline 65501fbb 65501fb, candidate: this change; Apple M5 Max, macOS 27.0.1, Xcode 27.2; 795.4 s

game status shaders (base → cand) newly failing newly building changed MSL interface (new / known) boot fps p50 frame p99 ms peak MB prepare s new RED
SuperMarioRPG 0100BC0018138000 REGRESSION (intended picture change) 82032/82032 → 82032/82032 0 0 0 0 / 0 ok 59.0 → 59.8 35.96 → 24.88 5000 → 4939 0.26 → 0.25 0
  • screenshots (s: dHash bits / RGB diff / baseline noise): 30: 0/0.36/0, 55: 27/38.57/0, 70: 27/38.55/0
  • baseline: first frame 3.0 s, max frame 4518, reached, prebuild 280.9 s (40973 built, 0 failed), fps 59.0, p50/p99 16.5/35.96 ms, peak 5000 MB
  • candidate: first frame 2.4 s, max frame 4552, reached, prebuild 282.8 s (40973 built, 0 failed), fps 59.8, p50/p99 16.56/24.88 ms, peak 4939 MB

The earlier chain-only check on both local games (Super Mario RPG and 13 Sentinels) passed: 82032/82032 and 751/751 shaders, no changed MSL, no interface failures.

Docs

  • docs/reference/rendering.md has a new entry, "RGBA4 textures in Metal's channel order". The symptom index is regenerated.
  • Super Mario RPG's note links the entry under "Found on this game" and describes the Mushroom Way route under "Mac routes".

Reverse all four nibbles when guest RGBA4 storage crosses into native Metal textures. Keep guest-backed sampling, copies, raw format views and writeback coherent, and cover those paths with literal GPU regressions. Document the reproduced dark Mushroom Way battle and corrected scene.
Track written mip levels, array slices and depth planes so converted views do not overwrite independent native writes. Close attachment conversion before auxiliary clears, invalidate views after texture-to-buffer copies, and resolve inverse RGBA4 integer images before checking write usage. Add literal GPU regressions for all four reviewed failures.
@MKLOL
MKLOL force-pushed the codex/nvn-rgba4-component-order branch from ada5e80 to 1f493a4 Compare November 2, 2026 23:37
@MKLOL
MKLOL marked this pull request as ready for review November 3, 2026 01:49
…ersed

The earlier commits converted RGBA4 through a representation layer of its own for format views, integer images, scaled copies and 3D mirrors: cases no game is known to reach. RGBA4 now uses the existing packed-format crossings (uploads, buffer <-> texture copies, ReadTexels, storage readbacks, and the in-place mapping of linear and buffer textures), reversing the four nibbles where the RGB565 family exchanges red and blue. Same-size views in another format keep the RGB565 family's documented limit. Main's own clang-tidy fixture findings are left to their pull requests (seedds#958, seedds#959, seedds#979).

Super Mario RPG gets a smoke route from a save on Mushroom Way into the battle the fix lights, so regress.py plays it.
@MKLOL
MKLOL force-pushed the codex/nvn-rgba4-component-order branch from 1f493a4 to 3898bf6 Compare November 3, 2026 02:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants