fix(vk): storage buffers on 16-byte boundaries and bounds-checked texture decode stores (AMD, ported from Fritchii) #2442

Merged
hckrman101 merged 3 commits from port/fritchii-rendering into main 2026-11-11 21:51:25 +00:00
hckrman101 commented 2026-11-11 19:23:13 +00:00 (Migrated from github.com)

Two AMD problems that Lichii and Fritchii found in code Hookshot shares with them. Storage buffers bound at AMD's 4-byte alignment can make 8- and 16-byte shader reads return the wrong words. The ASTC decoder and the block-linear unswizzle store texels past the edge of the image.

What changes

  • Storage buffers bound and translated at 16 bytes at least (storage_binding_align in gpu/vk/vk_internal.h, used by make_profile/host_info and the descriptor writer).
    • The translator reads 8- and 16-byte storage accesses through vector views (offset >> 3, >> 4). These are exact only when the binding starts on a 16-byte boundary.
    • Bindings started at the device's own alignment. On NVIDIA that is 16, but on AMD it is 4. An aligned access could then start at any word of the binding, and the view read the vector before it.
    • In OoT that shifted the light colours (green lighting, cyan sky), as found for Lichii: zortos293/Lichii#295, from Matt Artist (mja00/Lichii 5e8d409f81) and sharkiix (c418e4f477), in Fritchii as 57ed438cd.
    • Lichii fixed it in the translator, splitting such accesses into words. Here AMD simply binds the way NVIDIA already does. On AMD a binding, including one the shader writes, now starts up to 12 bytes before the address the game gave; the shader's offsets are counted from there. NVIDIA, at 16, already worked this way.
    • Mesa's ANV (Intel on Linux) also reports 4-byte alignment, so it gets the new translation too.
    • The program cache's stamp changes (vk_shader.cpp), so players prepare their shaders once again, before play. Nothing compiles during play.
    • The stamp now also includes the storage alignment the translation was made with. A folder's cache translated on a 16-aligned GPU is then not reused on a 64-aligned one, as can happen with a laptop's integrated and dedicated GPUs.
  • ASTC decode and block-linear unswizzle stores checked against the image's size (gpu/vk/host_shaders/astc_decoder.comp, block_linear_unswizzle_2d.comp; SPIR-V headers regenerated with scripts/host_shaders.py).
    • A block at the right or bottom edge of an image whose size is not a multiple of the block size wrote outside the image. So did the 32×32 unswizzle workgroups that run past a mip level.
    • robustImageAccess2 is off, so those stores are undefined. NVIDIA drops them. On AMD they landed in other images' memory: black blocks and a crosshatch on an RX 6750 XT, and in the end a GPU fault.
    • Ported from Fritchii f52631cf6 by Frizti, from CheezerWheezer's Lichii build. The host shaders are not part of the program cache's stamp.

Verified (RTX 3090, Linux, my own dump)

  • The prepared program cache is byte-identical to main's on NVIDIA: 0 of 53,412 program records change. NVIDIA's alignment is already 16, so only the stamp moves.
    • As a local test only, I forced AMD's 4-byte alignment on the same NVIDIA card. 40,212 records then translate differently.
    • The game still looked the same at Kokiri Forest and Lake Hylia with 4-byte bindings, so I could not reproduce the colour shift on NVIDIA. Its reach on AMD is left for an AMD run.
    • A local count of the game's storage bindings (Kokiri Forest, then Lake Hylia): 11,744 of 18.4 million start off an 8-byte boundary, all of them 4-byte aligned. With AMD's 4-byte alignment, an 8- or 16-byte access through those bindings reads the wrong vector. That is rare, which fits the colour shift not being visible in these two places.
  • tests/test_amd_subgroup_profiles.py now also checks that the profile's storage alignment is 16 for a device that allows 4, that 64 stays 64, and that the alignment is in the program cache's stamp. tests/test_program_stamp_knobs.py passes.
  • tests/test_host_shaders.py passes: the regenerated headers match their sources. Both shaders pass spirv-val.
  • In game at Lake Hylia, main against this PR: no visible difference. lake
  • tests/run_all.py: everything passes except 4 checks (13 skipped). Those 4 (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

  • AMD and Intel GPUs: neither the colour shift nor the decode corruption was reproduced here, and both are AMD-only. An AMD run is planned on another PC.
  • Windows.
Two AMD problems that Lichii and Fritchii found in code Hookshot shares with them. Storage buffers bound at AMD's 4-byte alignment can make 8- and 16-byte shader reads return the wrong words. The ASTC decoder and the block-linear unswizzle store texels past the edge of the image. ### What changes - **Storage buffers bound and translated at 16 bytes at least** (`storage_binding_align` in `gpu/vk/vk_internal.h`, used by `make_profile`/`host_info` and the descriptor writer). - The translator reads 8- and 16-byte storage accesses through vector views (`offset >> 3`, `>> 4`). These are exact only when the binding starts on a 16-byte boundary. - Bindings started at the device's own alignment. On NVIDIA that is 16, but on AMD it is 4. An aligned access could then start at any word of the binding, and the view read the vector before it. - In OoT that shifted the light colours (green lighting, cyan sky), as found for Lichii: zortos293/Lichii#295, from Matt Artist (mja00/Lichii 5e8d409f81) and sharkiix (c418e4f477), in Fritchii as [57ed438cd](https://github.com/Frizti/Fritchii/commit/57ed438cdccb597b41e2cf7d01e9d93bc80a8e4d). - Lichii fixed it in the translator, splitting such accesses into words. Here AMD simply binds the way NVIDIA already does. On AMD a binding, including one the shader writes, now starts up to 12 bytes before the address the game gave; the shader's offsets are counted from there. NVIDIA, at 16, already worked this way. - Mesa's ANV (Intel on Linux) also reports 4-byte alignment, so it gets the new translation too. - The program cache's stamp changes (`vk_shader.cpp`), so players prepare their shaders once again, before play. Nothing compiles during play. - The stamp now also includes the storage alignment the translation was made with. A folder's cache translated on a 16-aligned GPU is then not reused on a 64-aligned one, as can happen with a laptop's integrated and dedicated GPUs. - **ASTC decode and block-linear unswizzle stores checked against the image's size** (`gpu/vk/host_shaders/astc_decoder.comp`, `block_linear_unswizzle_2d.comp`; SPIR-V headers regenerated with `scripts/host_shaders.py`). - A block at the right or bottom edge of an image whose size is not a multiple of the block size wrote outside the image. So did the 32×32 unswizzle workgroups that run past a mip level. - `robustImageAccess2` is off, so those stores are undefined. NVIDIA drops them. On AMD they landed in other images' memory: black blocks and a crosshatch on an RX 6750 XT, and in the end a GPU fault. - Ported from Fritchii [f52631cf6](https://github.com/Frizti/Fritchii/commit/f52631cf66b3a5014b94c58ebb4b2e2bb938f896) by Frizti, from CheezerWheezer's Lichii build. The host shaders are not part of the program cache's stamp. ### Verified (RTX 3090, Linux, my own dump) - The prepared program cache is **byte-identical to main's on NVIDIA**: 0 of 53,412 program records change. NVIDIA's alignment is already 16, so only the stamp moves. - As a local test only, I forced AMD's 4-byte alignment on the same NVIDIA card. 40,212 records then translate differently. - The game still looked the same at Kokiri Forest and Lake Hylia with 4-byte bindings, so I could not reproduce the colour shift on NVIDIA. Its reach on AMD is left for an AMD run. - A local count of the game's storage bindings (Kokiri Forest, then Lake Hylia): 11,744 of 18.4 million start off an 8-byte boundary, all of them 4-byte aligned. With AMD's 4-byte alignment, an 8- or 16-byte access through those bindings reads the wrong vector. That is rare, which fits the colour shift not being visible in these two places. - `tests/test_amd_subgroup_profiles.py` now also checks that the profile's storage alignment is 16 for a device that allows 4, that 64 stays 64, and that the alignment is in the program cache's stamp. `tests/test_program_stamp_knobs.py` passes. - `tests/test_host_shaders.py` passes: the regenerated headers match their sources. Both shaders pass `spirv-val`. - In game at Lake Hylia, main against this PR: no visible difference. ![lake](https://raw.githubusercontent.com/hckrman101/Hookshot/3d4e96e58c2aa91c6a8536596e9a6cf8855d291e/fritchii/rendering-lake-hylia-main-vs-pr.png) - `tests/run_all.py`: everything passes except 4 checks (13 skipped). Those 4 (`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 - AMD and Intel GPUs: neither the colour shift nor the decode corruption was reproduced here, and both are AMD-only. An AMD run is planned on another PC. - Windows.
hckrman101 commented 2026-11-11 20:59:50 +00:00 (Migrated from github.com)

Tested on AMD: RX 580 (Polaris), Mesa 26.2.6 RADV, Linux. This PR (2ea12bd4) against its base main 3e95ca68, at the same spots: Kokiri Forest, then a teleport to Lake Hylia.

RADV on this card reports minStorageBufferOffsetAlignment = 4, so this PR does change the translation here.

  • Lighting and colours: no difference. The base has no green lighting in Kokiri and no cyan sky, so this card doesn't show the bug. Lake Hylia's water, cliffs and sky look the same in both.
  • No texture-decode garbage, driver faults, VK_ERROR or device loss in either run.
  • Play counters: base 1,503 pipelines, this PR 1,507. VRAM 6.7–6.8 of 7.7 GB in both.

Left is the base, right is this PR:

Kokiri
Lake Hylia

So: no regression on Polaris/RADV, but no symptom here for it to fix either. The light-colour bug was reported on RX 6000/7000 cards, possibly on Windows, and needs one of those to confirm the fix.

The precompile was skipped in both runs, because it runs out of memory on this card on every build, main included (two mesh-fallback compute programs never finish compiling in ACO). That's being looked into separately.

Tested on AMD: RX 580 (Polaris), Mesa 26.2.6 RADV, Linux. This PR (2ea12bd4) against its base main 3e95ca68, at the same spots: Kokiri Forest, then a teleport to Lake Hylia. RADV on this card reports `minStorageBufferOffsetAlignment = 4`, so this PR does change the translation here. - **Lighting and colours:** no difference. The base has no green lighting in Kokiri and no cyan sky, so this card doesn't show the bug. Lake Hylia's water, cliffs and sky look the same in both. - **No texture-decode garbage, driver faults, `VK_ERROR` or device loss** in either run. - **Play counters:** base 1,503 pipelines, this PR 1,507. VRAM 6.7–6.8 of 7.7 GB in both. Left is the base, right is this PR: ![Kokiri](https://raw.githubusercontent.com/hckrman101/Hookshot/dc253f144ba70b645c118dca466404cc83fa54ba/amd-2442/cmp-3e95-2442-kokiri2.png) ![Lake Hylia](https://raw.githubusercontent.com/hckrman101/Hookshot/dc253f144ba70b645c118dca466404cc83fa54ba/amd-2442/cmp-3e95-2442-lake.png) So: no regression on Polaris/RADV, but no symptom here for it to fix either. The light-colour bug was reported on RX 6000/7000 cards, possibly on Windows, and needs one of those to confirm the fix. The precompile was skipped in both runs, because it runs out of memory on this card on every build, main included (two mesh-fallback compute programs never finish compiling in ACO). That's being looked into separately.
flavorfulroom commented 2026-11-11 21:53:37 +00:00 (Migrated from github.com)
<img width="616" height="99" alt="image" src="https://github.com/user-attachments/assets/5ec228a0-dcfe-433a-bab1-2d9df4b58b04" /> https://discord.com/channels/1568810484818378804/1569019786766188665/1570192658532536442
Commenting is not possible because the repository is archived.
No description provided.