fix(shaders): lower TLD.MS (multisampled texel fetch) to Maxwell TLDS 2D.LZ.MS #1769

Merged
Joey-Magnus merged 1 commit from fix/lower-tld-ms into main 2026-11-10 16:31:18 +00:00
Joey-Magnus commented 2026-11-10 16:16:56 +00:00 (Migrated from github.com)

Three of OoT's fragment programs fail to translate ("does not lower ... native texel fetch mode or descriptor binding") because the Ampere → Maxwell lowering refuses TLD with bit 78 set: a texel fetch from a multisampled texture. Whatever those programs draw is missing on every PC.

The encodings (from the startup pipeline build, the same on a Radeon 890M v2.0.13 log in #bug-reporting):

3dfaea49b4d5c57f +0x120  00087b66 30800809 049e4704 001fc200
f0b1c1757f776bde +0x100  040a7b66 30800809 049e4709 001fc200
c0707796557648a6 +0x160  10127b66 30800813 049e4716 001fc200

All three are TLD.SCR 2D, .LZ, RGB into Rd/Rd+1 plus a second destination, the descriptor at 0x8 of buffer 2. The only refused bit is 78 (.MS). Ra is even in all three (R0, R4, R16), and the first two share Rb (R9) at different Ra. That matches Maxwell's TLDS 2D.LZ.MS layout: x, y in Ra/Ra+1, the sample index in Rb. It is also how this lowering already treats bit 76 (offsets: Ra pair, offsets in Rb).

Change (lower_to_maxwell.h, case 0xb66): accept bit 78 for 2D at level zero and emit Maxwell TLDS encoding 6 (2D.LZ.MS). The Maxwell frontend already reads that as an ImageFetch with a sample (texture_load_swizzled.cpp), and texture_pass then marks the descriptor is_multisample, so SPIR-V declares an MS image and passes the Sample operand. Still refused: other dimensions, .LL, offsets together with .MS, an odd Ra, RZ as the sample, and .MS through a second descriptor buffer.

Tests

  • New tests/test_lower_tld_ms.py (in run_all.py): the three encodings lower to the expected TLDS 2D.LZ.MS and bind buffer 2; without bit 78 the same fetch lowers to plain 2D.LZ as before; the refused forms stay refused. It fails on main and passes here.
  • run_all.py: 246 passed, 0 failed.
  • In game (Linux, RTX 4070 Ti, 595.99.02): the startup pipeline build went from 10 programs that do not lower to 7, these three gone, with no pipeline or SPIR-V errors (0 programs (0 failed)).

I haven't yet found a scene that visibly uses these programs, so there's no before/after screenshot. If someone knows which effect reads a multisampled target in a fragment shader (an MSAA resolve or edge pass?), that's where to look.

🤖 Generated with Claude Code

Three of OoT's fragment programs fail to translate ("does not lower ... native texel fetch mode or descriptor binding") because the Ampere → Maxwell lowering refuses TLD with bit 78 set: a texel fetch from a multisampled texture. Whatever those programs draw is missing on every PC. **The encodings** (from the startup pipeline build, the same on a Radeon 890M v2.0.13 log in #bug-reporting): ``` 3dfaea49b4d5c57f +0x120 00087b66 30800809 049e4704 001fc200 f0b1c1757f776bde +0x100 040a7b66 30800809 049e4709 001fc200 c0707796557648a6 +0x160 10127b66 30800813 049e4716 001fc200 ``` All three are TLD.SCR 2D, `.LZ`, RGB into Rd/Rd+1 plus a second destination, the descriptor at 0x8 of buffer 2. The only refused bit is 78 (`.MS`). Ra is even in all three (R0, R4, R16), and the first two share Rb (R9) at different Ra. That matches Maxwell's TLDS 2D.LZ.MS layout: x, y in Ra/Ra+1, the sample index in Rb. It is also how this lowering already treats bit 76 (offsets: Ra pair, offsets in Rb). **Change** (`lower_to_maxwell.h`, case 0xb66): accept bit 78 for 2D at level zero and emit Maxwell TLDS encoding 6 (2D.LZ.MS). The Maxwell frontend already reads that as an `ImageFetch` with a sample (`texture_load_swizzled.cpp`), and `texture_pass` then marks the descriptor `is_multisample`, so SPIR-V declares an MS image and passes the `Sample` operand. Still refused: other dimensions, `.LL`, offsets together with `.MS`, an odd Ra, RZ as the sample, and `.MS` through a second descriptor buffer. **Tests** - New `tests/test_lower_tld_ms.py` (in `run_all.py`): the three encodings lower to the expected TLDS 2D.LZ.MS and bind buffer 2; without bit 78 the same fetch lowers to plain 2D.LZ as before; the refused forms stay refused. It fails on `main` and passes here. - `run_all.py`: 246 passed, 0 failed. - In game (Linux, RTX 4070 Ti, 595.99.02): the startup pipeline build went from 10 programs that do not lower to 7, these three gone, with no pipeline or SPIR-V errors (`0 programs (0 failed)`). I haven't yet found a scene that visibly uses these programs, so there's no before/after screenshot. If someone knows which effect reads a multisampled target in a fragment shader (an MSAA resolve or edge pass?), that's where to look. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
hckrman101 (Migrated from github.com) approved these changes 2026-11-10 16:20:57 +00:00
hckrman101 (Migrated from github.com) left a comment

Looks right to me. I checked the encoding against the Maxwell side: texture_load_swizzled.cpp reads TLDS encoding 6 as Color2D with x, y in Ra, Ra+1 and the sample in Rb, which is what this emits. The test's bit positions also line up (bit 78 = word 2 bit 14, & ~5U still lets only 76 and 78 through). MS arrays, 1D/3D, .LL, offsets with .MS, odd Ra and RZ as the sample all still fall through to the existing failure instead of being lowered wrong.

Downstream is fine too: texture_pass sets is_multisample from the fetch's sample arg, EmitImageFetch drops the Lod operand when there's a sample, so no illegal Lod+Sample on an MS image, and TextureDescriptor is serialized as POD, so the flag survives the program cache. On the host, guest MSAA textures become samples > 1 images with SAMPLED usage, and the dummy is a null descriptor, so the fallback doesn't introduce a sample-count mismatch.

Small things, none blocking:

  • The draw-time binding doesn't check d.is_multisample against the view it binds. That's fine as long as the game binds a real MSAA texture here, but when you find the scene that uses these programs, one run with the validation layer would confirm the bound view really is multisampled.
  • Nit: the second-descriptor-buffer refusal (inline) has no test case.

CI is still running at the moment.

Looks right to me. I checked the encoding against the Maxwell side: `texture_load_swizzled.cpp` reads TLDS encoding 6 as Color2D with x, y in Ra, Ra+1 and the sample in Rb, which is what this emits. The test's bit positions also line up (bit 78 = word 2 bit 14, `& ~5U` still lets only 76 and 78 through). MS arrays, 1D/3D, `.LL`, offsets with `.MS`, odd Ra and RZ as the sample all still fall through to the existing failure instead of being lowered wrong. Downstream is fine too: `texture_pass` sets `is_multisample` from the fetch's sample arg, `EmitImageFetch` drops the Lod operand when there's a sample, so no illegal Lod+Sample on an MS image, and `TextureDescriptor` is serialized as POD, so the flag survives the program cache. On the host, guest MSAA textures become `samples > 1` images with SAMPLED usage, and the dummy is a null descriptor, so the fallback doesn't introduce a sample-count mismatch. Small things, none blocking: - The draw-time binding doesn't check `d.is_multisample` against the view it binds. That's fine as long as the game binds a real MSAA texture here, but when you find the scene that uses these programs, one run with the validation layer would confirm the bound view really is multisampled. - Nit: the second-descriptor-buffer refusal (inline) has no test case. CI is still running at the moment.
hckrman101 (Migrated from github.com) commented 2026-11-10 16:20:57 +00:00

Nit: this multisample refusal for a second descriptor buffer isn't covered by test_lower_tld_ms.py. A case that binds buffer 2 with a plain TLD first and then the MS fetch from another buffer would pin it down. The TLD.B fallback below would otherwise silently drop the sample index.

Nit: this `multisample` refusal for a second descriptor buffer isn't covered by `test_lower_tld_ms.py`. A case that binds buffer 2 with a plain TLD first and then the MS fetch from another buffer would pin it down. The TLD.B fallback below would otherwise silently drop the sample index.
Commenting is not possible because the repository is archived.
No description provided.