fix(shaders): F2I.S32 and I2F.U32.RZ conversions, LDSM address copy (ported from Fritchii) #2443
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!2443
Loading…
Reference in a new issue
No description provided.
Delete branch "port/fritchii-shaders"
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?
Some of the game's shaders hand the driver a conversion whose result SPIR-V leaves undefined, or round the wrong way, and one LDSM reads the wrong shared-memory rows. These are fixes Lichii and Fritchii found in the same Eden shader translator; I checked each one against the game's own shaders before porting it.
What changes
INT32_MAXas a float, which rounds up to 2^31. An input of 2^31 or more therefore reachedOpConvertFToSout of range, and SPIR-V leaves that undefined. The clamp now stops at the largest float below 2^31, andINT32_MAXis selected for inputs at or above 2^31. NaN still gives 0. Ported from Fritchii eba45b151 by Frizti, which ports zortos293/Lichii#1051 by myshadowww.OpConvertUToFrounds to nearest. Values of 2^24 or more could round up, for example 0xffffffff to 2^32 instead of 0x4f7fffff, so a hash times 2^-32 could reach 1.0. The conversion is now built from the top 24 significant bits, and is exact below 2^24. Ported from Fritchii f6a8f5132 by zortos293.clip_distance_writtenwas astaticin a header, so each file had its own copy. The prologue's copy was never set, and translation threads wrote the other one at the same time. The prologue now zeroes the clip distances on purpose. The SPIR-V is unchanged. Ported from Fritchii ff0f6aa30 by Frizti (zortos293/Lichii#902).The translator changes, so the program cache's stamp changes. Every player prepares shaders once again, before play, as after any translator update. Nothing is compiled during play.
How the fixes were chosen
I reviewed every commit on Fritchii's active branch since 2026-10-01 that touches the shader recompiler or the host shaders: 240 commits, Fritchii's own and the Lichii and Eglantine commits it carries. Each was matched against the corresponding code path in Hookshot's translator.
I then ran the Ampere-to-Maxwell lowering offline over all 42,856 of the game's stages, using my own dump and keeping everything local. Every stage lowers except the HMMA networks, which take the native matrix path at run time. That makes the roughly 120 commits that only add instruction forms for other games irrelevant here. The rest are either already in Hookshot or are latent: the game's shaders never reach them (for example
IsGenericstopping at Generic31X, or a fragment PrimitiveId needing the Geometry capability). These four are the ones the game's shaders do reach.Verified
tests/f2i_immediategains value checks. Literal inputs go through the real lowering, the Maxwell translation and the production IR passes. The remaining IR is then evaluated as SPIR-V defines each instruction, and any conversion that receives an out-of-range value is reported.spirv-val. F2I.S32 turns 2^31, 2^32, the largest float and +inf into 0x7fffffff, -2^31 and -inf into 0x80000000, NaN into 0, and -13.75 into -13. I2F.RZ turns 0xffffffff and 0xffffff80 into 0x4f7fffff, and 0x01000001 into 0x4b800000.OpConvertFToSan out-of-range value, and the four large I2F inputs round to nearest.gpu/vk/tests/ldsm_address_test.cpp, run bytests/run_all.py, lowers LDSM for every count, both layouts, and address registers inside and outside the destinations. It checks that every shuffle reads the address copy and that nothing overwrites the copy before the last shuffle. On main it fails every overlapping .x2/.x4 non-transposed case; with this PR it passes.tests/run_all.py: 357 passed, 13 skipped. The 4 failures (test_wayland_libdecor_hint,test_nup_read_name,test_bake_portability,test_builder_log_read_only) fail the same way on main on this machine.Not tested
Checked each change against the source:
0x1.fffffep30fis 2^31-128, the largest float below 2^31. The Select comparesop_aafter abs/neg, which is the value converted; floats in [2^30, 2^31) are already integers, so rounding can't carry across the bound. The compare is ordered, so NaN skips it and still reaches the NaN-to-0 select.FindUMsb(0)is guarded, msb <= 23 converts the masked source exactly, and neg is applied after, which is right since RZ is symmetric.dest + count > 246keeps the address below the scratch range.structured_control_flow.cpp:684), so the shader's own stores win. The SPIR-V really is unchanged and the cross-thread race goes away.Follow-ups, not regressions: F2I.U32 from F32 has the same problem (
static_cast<f32>(UINT32_MAX)is 2^32, out of range forOpConvertFToU,floating_point_conversion_integer.cpp:159), as does F32 to S64. RM/RP rounding and signed RZ still round to nearest, because the SPIR-V convert never readsfp_control.rounding.github-sync referenced this pull request2026-11-12 06:22:54 +00:00