Skip to content

fix(vk): RADV on GCN 1-4 skips the two mesh fallback programs its compiler never finishes - #2525

Merged
hckrman101 merged 3 commits into
NotRexed:mainfrom
hckrman101:fix/radv-gcn-mesh-fallback
Nov 11, 2026
Merged

hckrman101 merged 3 commits into
NotRexed:mainfrom
hckrman101:fix/radv-gcn-mesh-fallback

Conversation

@hckrman101

Copy link
Copy Markdown
Collaborator

Why

On an RX 580 (Polaris 10, Mesa 26.2.6 RADV), a cold --prepare-shaders got through the translation and the prediction, then ran out of memory in the precompile. It grew past 24 GB and was killed, on main too. The game's own first start does the same precompile, so a player with an RX 400/500-class card can't get through the first start: it pushes the desktop into swap or the OOM killer.

The cause is two mesh programs' fallback compute pipelines, 93e3e999f2fb9fd8 (stage 1) and ea4dd64df9c8f2b7 (stage 0). RADV never finishes compiling them on GCN 1-4 (GFX6-8), and its thread keeps the memory it grew to. A time limit wouldn't help for that reason.

What makes them different from the other mesh programs:

  • Size. Both mesh stages are 4.3 MB of SPIR-V: about 28,000 blocks and 80,000 phis. The native programs already have about 11,000 blocks each. The next biggest mesh programs have up to 29,400 phis.
  • Live values. About 400 values are live at once (≈300 32-bit, ≈100 booleans), far more than a wave's 256 VGPRs and ~100 SGPRs. ACO has to spill tens of thousands of them.
  • Not the 126 vertices. Their max vertices/primitives of 126 isn't what sets them apart: 246 of the first 400 mesh programs the prepare logs are 126/126, and they build normally.
  • Not the fallback's own code. The CFG comes from the native program's structurization, which the native mesh path shares.

I compiled the dumped modules with RADV itself, through Mesa's amdgpu drm-shim (no AMD GPU needed), under a 6 GB cap:

module Polaris 10 / 12, Stoney, Bonaire, Pitcairn (GFX6-8) Vega 10, Raven (GFX9) Navi 10 / 21
ea4dd64df9c8f2b7 mesh (79,947 phis) not done after 45-90 s, killed at the 6 GB cap (the lab: 25 GB at 150 s) 14 s, 2.9 GB, 24,879 SGPR + 10,644 VGPR spills 13-14 s, 2.9 GB
ecbbff63280b68e2 mesh (29,372 phis) 4.5 s, 480 MB
cf987f56b971bdb3 mesh (28,506 phis) 5.5 s, 664 MB

RX_VK_NOOPT (DISABLE_OPTIMIZATION) doesn't change it. The spiller runs either way. GFX6-8 have no scratch instructions. ACO spills there through buffer instructions, and these programs' scratch is past their offset range, so ACO takes its overflow path for every spill and reload (aco_spill.cpp). With this many spills it doesn't finish. That's my reading of the source, not profiled.

I couldn't find a translation-side change that compiles these two normally. Their register pressure and block count come from the native program and the shared goto-elimination structurizer. Changing either would retranslate every shader for every GPU. So this PR leaves just these two out on the cards whose compiler can't build them.

What changes

  • gpu/vk/fallback_size_policy.h (new): spirv_phis() counts a module's OpPhi instructions. too_big() decides whether a mesh fallback compute stage should be skipped: the device is RADV on GCN 1-4, and the stage has more than 50,000 phis. 50,000 sits between the 29,400 that build in seconds and the 80,000 that never finish.
  • vk_progcache.cpp: each stage's phi count is recorded where its SPIR-V passes through the program cache. That is on save, which every translated program goes through, and on read. It's done there and not in vk_shader.cpp because vk_shader.cpp is part of the translator stamp, and touching it would retranslate every player's programs. The record format is unchanged.
  • vk_main.cpp / vk_internal.h: dev.radv_gcn is set when the driver is RADV and storageInputOutput16 is false. RADV sets that feature from packed 16-bit math, which means GFX9 and later. Checked through the drm-shim: false on Polaris and Pitcairn, true on Vega 10 and Navi 10.
  • vk_draw.cpp fallback_compute_pipelines: before anything is built, a program with a stage over the limit goes into the same "failed" set as a pipeline the driver refuses. It logs one line, and its draws are left out (as fix(vk): mesh fallback grass is left out while its shaders build, not drawn as holes #2382 leaves out draws whose fallback isn't ready). The precompile and the in-play builds both go through this function.
  • RX_VK_FALLBACK_PHI_LIMIT=<n> sets another limit, and 0 turns it off. It's documented in docs/development.md.
  • Tests: tests/test_fallback_size_policy.py checks the counter on well-formed, cut-short and non-SPIR-V input, checks the limit, and checks that the source still calls the policy before any fallback pipeline is built. program_cache_cleanup_test.cpp now includes the header.

What's lost: on RADV with an RX 400/500, R9 2xx/3xx or Fury card, the draws of these two mesh programs. Neither was drawn in the lab's Kokiri Forest or Hyrule Field runs, so I don't know yet which objects they are. Nothing changes on Vega and later, on AMD's Windows driver, on NVIDIA, or on Intel. The SPIR-V of every program is unchanged.

Verified

  • Dumped the mesh fallback SPIR-V on Linux (RADV Polaris 10 through the drm-shim, RX_VK_DUMP_MESH_SPV). Both modules pass spirv-val --target-env vulkan1.3 --uniform-buffer-standard-layout --scalar-block-layout. This PR doesn't change any SPIR-V.
  • Ran the precompile on the drm-shim's Polaris 10, translating only these two and the two biggest mesh programs that do build (a local-only key filter), with a 6 GB cap:
    • main: OOM-killed at the 6 GB cap after 12 s.
    • this PR: completes in 16 s with the two log lines (79,947 phis, the most 50,000) and the other two fallback programs built.
  • Ran the phi count over all ~800 mesh fallback modules: only these two are over 50,000.
  • Ran python -I -B tests/run_all.py fallback_size_policy program_cache_cleanup mesh_fallback mesh_fallback_failed_once program_cache_reader driver_faults run_all_declarations (8 passed), plus the program cache, prediction and recorder tests (all pass). hookshot_vk.so builds on the current main, ABI check ok.

Not tested

  • A real RX 580: no full cold prepare + precompile with this build yet.
  • Windows, and AMD's own driver (the check is RADV only).
  • In play: which objects the two programs draw, and so what is missing on these cards.
  • A Mesa release with a fixed spiller would build them. The limit would then leave them out for no reason until RX_VK_FALLBACK_PHI_LIMIT=0 or a version check is added.

hckrman101 and others added 2 commits November 11, 2026 16:57
…piler never finishes

On an RX 580 (Polaris, RADV), the shader preparation's precompile ran out of
memory past 24 GB, on main too. Two mesh programs' fallback compute stages
(93e3e999f2fb9fd8 and ea4dd64df9c8f2b7, 4.3 MB of SPIR-V each: ~28,000 blocks,
~80,000 phis, ~400 values live at once) never finish in ACO's spiller on
GFX6-8, which grows without bound and keeps the memory on the driver's thread.
GFX9 and later build the same modules in ~14 s and 2.9 GB, and the next biggest
mesh programs (up to 29,400 phis) build on Polaris in 4-6 s and under 700 MB.

Each stage's OpPhi count is now noted where its SPIR-V passes through the
program cache (vk_progcache.cpp, outside the translator stamp), and
fallback_compute_pipelines leaves a program out, as a refused one, when the
device is RADV on GCN 1-4 (no storageInputOutput16) and a compute stage has more
than 50,000 phis. RX_VK_FALLBACK_PHI_LIMIT changes the limit (0: none).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y whether the GPU is RADV on GCN 1-4

A value that is not a number from 0 to 4294967295 ("off", "-1", "12k") was read by
strtoul as no limit or a wrapped one. It is now ignored with a warning, and the
limit in use is logged once when the fallback first builds on such a card (or
whenever the variable is set). The subgroups line says "RADV on GCN 1-4: yes/no".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hckrman101

Copy link
Copy Markdown
Collaborator Author

Tested on the real card: RX 580 (Polaris 10), Mesa 26.2.6 RADV, i7-9700K, 15 GB. This PR (42d84fc) built natively, with a cold --prepare-shaders (empty caches, the precompile on), capped at 12 GB RAM + 4 GB swap.

The prepare finishes. On main it was killed in the precompile every time, past 24 GB.

Step Time
Translation 392.6 s (38,809 programs)
Prediction 1,175.9 s (55,252 pipelines made)
Precompile 393.3 s: 17,553 graphics + 9,683 compute pipelines, 1,096 mesh fallback programs
Total 1,966 s, exit code 0

Peak memory: 10.6 GB (8.4 GB RAM + 2.1 GB swap, at the start of the precompile).

The log has RADV on GCN 1-4: yes, and exactly the two expected skip lines:

vk: fallback: the compute pipeline of 93e3e999f2fb9fd8 (stage 1 of 4, 79947 phis, the most 50000) is not built: RADV on this GCN card would not finish it (RX_VK_FALLBACK_PHI_LIMIT); its draws are left out
vk: fallback: the compute pipeline of ea4dd64df9c8f2b7 (stage 0 of 3, 79947 phis, the most 50000) is not built: RADV on this GCN card would not finish it (RX_VK_FALLBACK_PHI_LIMIT); its draws are left out

A warm second start and a play run in Kokiri and Hyrule Field follow; I'll add them here.

@hckrman101
hckrman101 marked this pull request as ready for review November 11, 2026 23:05
@hckrman101

Copy link
Copy Markdown
Collaborator Author

The rest of the RX 580 check:

  • Warm second start: clean, with no "did not get through the precompile" line. The two skip lines appear again at once. The precompile takes 15.8 s, 80 s for the whole start, with a 9.4 GB peak (mostly page cache).
  • Play (about 70 s in Kokiri, then a teleport to Hyrule Field for 60 s):
    • reached play in 122 s;
    • 0 draws left out, and 0 programs translated during play;
    • only 867 ms of pipeline builds during play. Main's runs with the precompile skipped spent 2–4 s;
    • nothing visibly missing compared with main at the same spots: Kokiri's houses, foliage, characters and fog, and the Lost Woods bridge exit.

So whatever the two skipped programs draw, it isn't in those two areas.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant