fix(shaders): two large compute programs lower (an immediate is not R254) and get their local memory #1800
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!1800
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/lower-immediate-not-register"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Two of OoT's largest compute programs (~32,000 instructions each) are refused as "native program uses lowering scratch registers", although they use no scratch register. The lowering mistakes an immediate operand's low byte for register R254. Once they lower, a second bug shows up: compute programs never get their local memory, so the game aborts at their first local access. This PR fixes both. The second commit must land with the first.
1. An immediate's low byte is not a register (
lower_to_maxwell.h)Both stop at
LOP3.LUT R231, R231, 0xfffffffe, RZ, 0xc0(e7e77812 fffffffe 078ec0ff 000fc600; the other hasR236 ... 0xfffffffd). With the uniform bank in use, R231 is a high GPR. The first loop over an instruction's fields decides whether it has high GPRs and counts only fields that hold registers (register_field: bit 32 only in the register form or for listed opcodes). The second loop, which marks which GPRs are high, went over every field, including the immediate at bit 32. So 0xfe became "R254", a scratch register, and the program was refused. The second loop now uses the sameregister_fieldtest (factored into a lambda).2. Compute programs get their local memory size (
vk_shader.cpp)The
Envsetlocal_memoryonly for graphics stages, from their header. Compute programs got 0, the SPIR-V back end declared no local array, and a compute program that spills (LDL/STL) aborted the game inSirit::OpAccessChainfromEmitLoadLocal(gdb backtrace:seed_compute→translate_into→EmitSPIRV→EmitLoadLocal). The size is in the NVN2 control section at +0x68, bytes a thread. I found it by dumping 400 compute control sections:parse_codenow reads it next to the shared memory size (+0x64).Tests
tests/test_lower_immediate_not_register.py: a uniform-address LDG (so the uniform bank is in use), then each LOP3 from the log, then EXIT. Refused onmain, lowered here. An immediate below the scratch range lowers either way.tests/test_compute_local_memory.py(source check): the compute branch ofparse_codereads +0x68, and theEnvgives it to compute programs.run_all.py: all pass exceptnx2/test_nup_stand_in.py, which is red onmain(fixed by #1790).🤖 Generated with Claude Code
Thanks, both diagnoses look right to me, and the log encodings make this easy to follow.
LOP3.LUT0x812 is form 4, so bit 32 is the immediate, and the first loop already ignores it. The local-memory half matches what Eden does:DefineLocalMemoryreturns early at size 0,local_memorystays Id 0, andEmitLoadLocal's access chain aborts. The shader stamp is a hash ofeden/+vk_shader.cpp(nx2/build.py), so nothing stale is reused from the program cache.One real concern, on the second loop (inline). The
register_fieldlist doesn't cover every opcode the lowering reads a register from at bit 32. Until now that gap only mattered when bit 32 was the only high field, because the second loop still marked it whenever another field setany_high. With the newcontinuethat fallback is gone, so a few instructions that lowered correctly before now lose their REGMAP for Rb.Smaller points:
+0x68is taken as-is. 393/395 zeros plus the+0x50 == 32 * +0x68relation is good evidence, but a cheap sanity check (e.g. require that relation, or cap the size and log) would stop a misread field from turning into a huge Private array and a driver compile stall on some other program. Also worth knowing (pre-existing, not this PR):LoadLocal's bounds check is aSelectafter the load, and STL with a dynamic offset isn't checked at all, so an undersized value means out-of-bounds Private accesses (UB), not clamping.test_lower_immediate_not_register.pyonly checks that the programs lower. A case like the TLD4 one below, asserting that the high Rb still gets its map prefix, would pin down the behaviour that changes here.test_compute_local_memory.pyis a source-text check; that's fine for this.tests/run_all.pyconflicts with thetest_lower_tld_ms.pyline from #1769 (adjacent lines; it will conflict the same way with #1778's entry). It's a trivial resolve. The.hand.cppmerge cleanly, and there's no overlap with #1778's LDG/STG hunks.This
continuechanges output for instructions that lowered correctly before.register_fieldis false at bit 32 for several opcodes whose lowering reads Rb from there: TLD40xb63/0xf63(source_b), TMML0xb69/0xf69, TEX0xf60, SHFL.IDX0x589(form 2, register lane at 32), and STL0x987in the first (non-extended) pass. Take TLD4 with uniform bank in use,Ra = R228,Rb = R230. Before, Ra setany_highand this loop also marked R230/R231native_high, so the MOV that copies Rb into R251 got the map prefix. Now R230 is skipped, and that MOV reads the plain register while the producer wrote the extended one. That's the same class of bug the ISBEWR/SUST/RED/TXD additions above fixed. It also breaks the "every program that lowered before lowers to the same words" invariant from theextended_renamingcomment.Two options: (a) add those opcodes to
register_field(that also closes the old gap where Rb is the only high field), or (b) skip here only when bit 32 is positively an immediate/cbuf (the ALU familiesoperand()reads, forms 2-5), so every other opcode keeps the conservative marking.@ -297,2 +309,4 @@c.local_memory = 0;}}c.ok = true;Nit: maybe sanity-check this before trusting it, e.g. that
+0x50 == 32 * +0x68, or cap it and log, so a field misread on some other control section can't produce a multi-MB Private array.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!
Thanks, good catch on TLD4/TMML/TEX/SHFL.IDX/STL. Changed in
f58c6c0:Ra = R228, Rb = R230case. Theregister_fieldrefactor is gone.RX_VK_DUMP_NATIVE, lowered each with main'slower_to_maxwell.hand with this branch's, and compared the full outputs (instructions + source offsets):FMUL R242, R242, 0x3e26c6de(form 4). Main listed R222 (the immediate's low byte 0xde) next to R242 in that instruction's prefix; now only R242. The instruction never reads R222, so the prefix just drops a bogus entry.run_all.py(the test declaresRUN_ALL_ARGS).run_all.py: 263 passed, 0 failed.Thanks for the quick turnaround. This looks good now.
Fixed:
three/twolists fromRenameableFields, plus ISETP/FSETP). In all of those,operand()reads bit 32 as a 32-bit immediate, so dropping it is correct. Every other opcode is marked the same way as on main, so the TLD4/TMML/TEX/SHFL.IDX/STL Rb case is back to before. ALU opcodes outside the list just keep the old conservative marking, so nothing regresses. Your 10,730-program comparison (identical except 6 REGMAP prefixes that drop a bogus R222) is convincing.FieldSpan, not this loop, and GitHub reports the PR as mergeable.Heads-up: #1830 (FIocker's super build, still open) carries both fixes as well. Its version of the first one skips on
!register_field, which is the broad form from your first revision and has the TLD4/TMML/... regression. Whichever PR lands second will conflict inlower_to_maxwell.handvk_shader.cpp. The narrow skip here is the one to keep.Remaining nits (not blocking):
RUN_ALL_ARGShardcodesg++(inline).Ra=R228, Rb=R230) still gets the map prefix. A one-line CHECK onregister_map_wordsor the MOV would lock in the narrowing.RenameableFields'three/twosets. A small shared helper would keep them from drifting apart.CI: two checks have passed (F2I immediate, SPIR-V narrow load/store). The rest haven't started yet.
@ -0,0 +13,4 @@import tempfilefrom pathlib import PathRUN_ALL_ARGS = ["--cxx", "g++"]Nit: run_all's placeholder would be better here:
RUN_ALL_ARGS = ["--cxx", "{cxx}"]. With a literalg++, a machine that only has clang++ (e.g. a Windows setup) skips the test instead of compiling it with the compilerfind_toolsfound.