fix(shaders): R2UR's source is one register, not a pair (a vertex program refused) #1806

Merged
Joey-Magnus merged 1 commit from fix/lower-r2ur-one-register into main 2026-11-10 18:02:34 +00:00
Joey-Magnus commented 2026-11-10 17:15:55 +00:00 (Migrated from github.com)

One of OoT's vertex programs (65dd5ce0adbe3b64) fails to translate ("native program uses lowering scratch registers") at an R2UR that reads R238. Nothing in it uses a scratch register: the lowering assumes R2UR's source may be a register pair.

Cause. R2UR UR6, R238 (ee0673c2 00000000 000e0000 000e2200). With the uniform bank in use, R238 is a high GPR. FieldSpan has no entry for R2UR, so the source's span came from its alignment: an even register, R4n+2, is taken as a pair, R238-R239. R239 is the lowering's own scratch, and R2UR lowers to eight words that use it, so that's a conflict. R2UR reads one 32-bit register.

Change (lower_to_maxwell.h, FieldSpan): R2UR's source spans one register, as MUFU's and CS2R's entries do for theirs.

Tests

  • New tests/test_lower_r2ur_one_register.py: R2UR UR6, R238 is refused on main and lowers here; R236 lowers either way.
  • run_all.py: all pass except nx2/test_nup_stand_in.py, red on main (fixed by #1790).
  • I lowered a dump of the program's native code (kept on my PC, not in the PR): it now lowers. In game (Linux, RTX 4070 Ti, 595.99.02), stacked on #1769/#1778/#1789/#1800: programs that do not lower 2 → 1, the startup build's failed programs 3 → 2.

🤖 Generated with Claude Code

One of OoT's vertex programs (`65dd5ce0adbe3b64`) fails to translate ("native program uses lowering scratch registers") at an R2UR that reads R238. Nothing in it uses a scratch register: the lowering assumes R2UR's source may be a register pair. **Cause.** `R2UR UR6, R238` (`ee0673c2 00000000 000e0000 000e2200`). With the uniform bank in use, R238 is a high GPR. `FieldSpan` has no entry for R2UR, so the source's span came from its alignment: an even register, R4n+2, is taken as a pair, R238-R239. R239 is the lowering's own scratch, and R2UR lowers to eight words that use it, so that's a conflict. R2UR reads one 32-bit register. **Change** (`lower_to_maxwell.h`, `FieldSpan`): R2UR's source spans one register, as MUFU's and CS2R's entries do for theirs. **Tests** - New `tests/test_lower_r2ur_one_register.py`: `R2UR UR6, R238` is refused on `main` and lowers here; `R236` lowers either way. - `run_all.py`: all pass except `nx2/test_nup_stand_in.py`, red on `main` (fixed by #1790). - I lowered a dump of the program's native code (kept on my PC, not in the PR): it now lowers. In game (Linux, RTX 4070 Ti, 595.99.02), stacked on #1769/#1778/#1789/#1800: programs that do not lower 2 → **1**, the startup build's failed programs 3 → 2. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
hckrman101 (Migrated from github.com) approved these changes 2026-11-10 17:19:14 +00:00
hckrman101 (Migrated from github.com) left a comment

Looks right to me. R2UR reads one register: the lowering only accepts the plain form (insn.words[1] == 0 && insn.words[2] == 0x000e0000, anything else is refused as "native register-to-uniform mode"), so no wider/modifier variant can reach the 1-register span. The SHFL it emits reads only Bits(24, 8), and its own words use R250/R251 and the target UR's home, not R238. The change is scoped to opcode 0x3c2 field 1, only on the extended-renaming retry. Fields 0/2/3 keep the old behavior: the UR destination never gets near reserved_base, and in the accepted form the bits at 32 and 64 are zero. R239 and R246+ still conflict (high_239 / high_in_scratch with words > 1), so real scratch overlaps are still refused.

No overlap with #1800: it changes the register_field loop (~L211-241), not FieldSpan. And bit 24 is a register field either way, so its continue doesn't skip this one.

Two small things:

  • The branch conflicts with main in tests/run_all.py: main added tests/test_lower_tld_ms.py on the same line after test_lower_suld_1d.py. It's a trivial rebase, keep both entries.
  • The R236 case (inline) isn't "as before". I ran main's real LowerToMaxwell on R2UR UR6, Rn: R236 is refused on main too (4-aligned, so its span was taken as a quad, R236-R239). So were R238, R239, R244 and R246. R230-R235, R237 and R240 lowered. So this PR also fixes R236 and R244. Worth fixing the comment/description, and maybe adding a negative check that R239 is still refused.
Looks right to me. R2UR reads one register: the lowering only accepts the plain form (`insn.words[1] == 0 && insn.words[2] == 0x000e0000`, anything else is refused as "native register-to-uniform mode"), so no wider/modifier variant can reach the 1-register span. The SHFL it emits reads only `Bits(24, 8)`, and its own words use R250/R251 and the target UR's home, not R238. The change is scoped to opcode 0x3c2 field 1, only on the extended-renaming retry. Fields 0/2/3 keep the old behavior: the UR destination never gets near `reserved_base`, and in the accepted form the bits at 32 and 64 are zero. R239 and R246+ still conflict (`high_239` / `high_in_scratch` with words > 1), so real scratch overlaps are still refused. No overlap with #1800: it changes the register_field loop (~L211-241), not `FieldSpan`. And bit 24 is a register field either way, so its `continue` doesn't skip this one. Two small things: - The branch conflicts with main in `tests/run_all.py`: main added `tests/test_lower_tld_ms.py` on the same line after `test_lower_suld_1d.py`. It's a trivial rebase, keep both entries. - The R236 case (inline) isn't "as before". I ran main's real `LowerToMaxwell` on `R2UR UR6, Rn`: R236 is refused on main too (4-aligned, so its span was taken as a quad, R236-R239). So were R238, R239, R244 and R246. R230-R235, R237 and R240 lowered. So this PR also fixes R236 and R244. Worth fixing the comment/description, and maybe adding a negative check that R239 is still refused.
@ -0,0 +1,57 @@
"""The Ampere -> Maxwell lowering takes R2UR's source as one register, not a pair (native op 0x3c2).
hckrman101 (Migrated from github.com) commented 2026-11-10 17:19:14 +00:00

Nit: on main this one is refused too. R236 is 4-aligned, so its span was taken as R236-R239, which reaches R239 (I checked by running main's lowering). So it's a second case this PR fixes, not an unchanged control. Maybe relabel it, and add CHECK(!lower(insn(0xef0673c2, 0x00000000, 0x000e0000, 0x000e2200))); // R239: still the scratch so the one-register span can't hide a real overlap.

Nit: on main this one is refused too. R236 is 4-aligned, so its span was taken as R236-R239, which reaches R239 (I checked by running main's lowering). So it's a second case this PR fixes, not an unchanged control. Maybe relabel it, and add `CHECK(!lower(insn(0xef0673c2, 0x00000000, 0x000e0000, 0x000e2200))); // R239: still the scratch` so the one-register span can't hide a real overlap.
Joey-Magnus commented 2026-11-10 17:48:28 +00:00 (Migrated from github.com)

Thanks, you're right about R236: it's 4-aligned, so main took it for the quad R236-R239 and refused it too. The test now calls it a second case this fixes, and checks that R2UR UR6, R239 is still refused (R239 itself is the scratch). Rebased onto main (the run_all.py conflict); run_all.py passes apart from nx2/test_nup_stand_in.py, which #1790 fixes.

Thanks, you're right about R236: it's 4-aligned, so main took it for the quad R236-R239 and refused it too. The test now calls it a second case this fixes, and checks that `R2UR UR6, R239` is still refused (R239 itself is the scratch). Rebased onto main (the `run_all.py` conflict); `run_all.py` passes apart from `nx2/test_nup_stand_in.py`, which #1790 fixes.
hckrman101 commented 2026-11-10 17:50:55 +00:00 (Migrated from github.com)

Heads-up: this conflicts with main now (a lot landed today). Could you merge main into the branch (or rebase) and resolve the conflicts? Thanks!

Heads-up: this conflicts with main now (a lot landed today). Could you merge main into the branch (or rebase) and resolve the conflicts? Thanks!
Commenting is not possible because the repository is archived.
No description provided.