fix(vk): search the storage buffer tables in a loop on Intel #2513
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!2513
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/intel-gm-table-loop"
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?
On Intel GPUs under Mesa, the game's culling programs never finish compiling, and Intel builds compute pipelines one at a time (issue 496), so every compute build queued behind them waits for good. In Kokiri Forest on an Iris Xe that leaves the forest grey, with no foliage or light, and the rocks and doors the mesh fallback draws stay white.
Part of #999 and #1001
Why it happens
A global memory access the storage buffer pass could not bind is translated into a search of the storage buffer tables: four tables of 16 entries, about 70 entries in all, each written out in
DefineGlobalMemoryFunctions. These helpers are markedDontInlinefor NVIDIA, but Mesa inlines every function anyway. The foliage culling program 53caac5f has 46 global accesses, so it ends up with over 100k instructions. At the 32 lanes compute is pinned to, Intel's compiler (brw, register allocation at SIMD32) did not finish it in 5+ minutes.What changed
spirv_emit_context.cpp: whenglobal_table_loopis set, each table is searched in a loop the driver is asked not to unroll (DontUnroll). The loop runs the same test as the written-out form, entry by entry: the same constant-buffer words, the same masked address and bounds, and the first match taken. Its counter is the same in every invocation that runs it, so this is dynamic indexing, not non-uniform, and modules with the loop declareStorageBufferArrayDynamicIndexing. The CAS loops' callers open their own block and keep the written-out form.vk_coopmat.cpp:table_loop_wanted()turns it on for Intel's Mesa driver (VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA), only where the device hasshaderStorageBufferArrayDynamicIndexing. The cause is Mesa's, so Intel's Windows driver, whose compiler is not Mesa's, keeps the written-out form until it has been tried.RX_VK_GM_LOOPoverrides the default on any GPU, read as every switch is (env_value_off).vk_shader.cpp:translator_stamp()takes the switch (flag bit 8). Programs cached in one form are not used in the other. The bit is 0 everywhere the loop is off, so on other GPUs the SPIR-V, the stamp and the cache are unchanged.docs/development.md: a row forRX_VK_GM_LOOP.The effect on the four table-heavy programs: 53caac5f's SPIR-V halves (998 KB to 448 KB), and each now builds in 12-15 s instead of not finishing.
Tests
tests/test_global_table_loop.py(new):table_loop_wantedand runs it for Intel's Mesa and Windows drivers, NVIDIA and AMD, with and without dynamic indexing andRX_VK_GM_LOOP(1,true,0);translator_stamptakes the switch;tests/global_load_16_ir.cpp: the global load fixture emits one module whose storage buffer table is searched in the loop, so the SPIR-V fixtures job runsspirv-valon the loop's form (its counter, the return inside its body).python -I -B tests/run_all.py global_table_loop bake_mimo amd_fragment_warp bake_skeleton bake_ds cas_loop_ceiling global_atomic_add_f32 mesh_point_topology prepared_stamps recorder_sync program_cache_format program_stamp_knobsgave 12 passed on Linux.test_mesh_unreachable_outputswas skipped (nospirv-valhere).Tested in the game
Iris Xe (Raptor Lake-P, i9-13900H, 32 GB), Mesa ANV 26.2.4, Bazzite. Kokiri Forest, walking out of Link's house:
RX_VK_PASS_TIMING=2), 20 ms of it the game's own dispatches. That work was never run before, so earlier frame rates in this scene are not comparable.Once the culling builds, the mesh fallback's colour draws still wait for their own (slow) builds. Until then the grass shows as sky-coloured holes, which bloom blows up into specks. #2382 holds those draws' depth while their colour programs build.
Not tested:
RX_VK_GM_LOOP=1turns the loop on there.RX_VK_GM_LOOP=1is set by hand.Overlap: #2306 also edits
DefineGlobalMemoryFunctions(the float atomics). The code merges cleanly with it; only thedocs/development.mdrow conflicts.Good find. The loop runs the same test as the written-out form: the same words (
cbuf_offset/8 + 2k), the same masked bounds, and it returns on the first match. Stamp bit 8 keeps the caches apart, and NVIDIA and AMD SPIR-V is unchanged.shaderStorageBufferArrayDynamicIndexingis also enabled whenever it's supported, becausevk_main.cpppasses the queried features intovkCreateDevice.gpu/vk/vk_coopmat.cpp:349usesstd::atoi(env) != 0. The convention isenv_value_off/env_on; withatoi,RX_VK_GM_LOOP=trueturns the loop off.DontInline. But the switch keys on vendor 0x8086, so Intel's Windows driver gets the loop too, untested.dev.driver_id == VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESAmatches what was tested; widen it once Windows has been tried.RX_VK_GM_LOOP=1would cover the return inside the loop body.Looks good. The second commit fixes all three of @Joey-Magnus's points. The switch now keys on
VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA, so Intel's Windows driver keeps the written-out form.RX_VK_GM_LOOPgoes throughenv_value_off, and the test coverstrue. The newcheck_table_loopfixture puts the loop form through spirv-val in CI. I checked the fixtures job log: the module is saved intoglobal-load-16/modulesand validated with the others.I went through the loop against the written-out form. It reads the same words (
cbuf_offset / 8 + 2kand the next one), masks the address the same way, applies the same size-0 rule and cap fromOpArrayLength, and returns on the first match. The control flow is structured: thecheckblock's conditional branch is a loop break, so it needs no merge, and the then-block ends in the callback's return. The counter'sOpVariableis emitted right after the function's first label, and only whendefine_bodyopens that label itself, so the CAS callers are left alone. The k-indexed descriptor access is dynamically uniform, and the module declaresStorageBufferArrayDynamicIndexing. The switch requires that feature, and the device enables it whenever it is supported. Stamp bit 8 keeps the two forms' caches apart. On other drivers the bit is 0, so their SPIR-V and caches don't change.CI is green, and the PR merges cleanly with main. The only overlap is the
docs/development.mdrow against #2306, as the description says. One small doc nit inline.@ -157,6 +157,7 @@ case-sensitive, and an unknown one is ignored.| `RX_VK_PRECOMPILE=0` or `1` | The shader preparation before the title off or on (on in builds made with `RX_RELEASE=1`, as the builders' are) || `RX_VK_TASK_MESH_FALLBACK=0` or `1` | Task + mesh draws through the compute fallback (1) or the driver's mesh shaders (0). By default the fallback is on for AMD's Navi 21 and Navi 22 on Windows (RX 6800, 6800 XT, 6900 XT, 6700 XT), where the native path timed out the driver (issue 996) || `RX_VK_DRIVER_FAULTS=0` or `2` | 0: programs the driver's compiler faulted on are asked of it again. By default they are left out for the rest of the run and, through `hookshot_driver_faults.txt` beside the executable, on later runs with the same driver, and on AMD one program's builds go to the driver one at a time (issue 996). 2: one at a time on every driver || `RX_VK_GM_LOOP=0` or `1` | The storage buffer tables of a global memory access searched in a loop (1) or written out entry by entry (0). On by default on Intel, whose compiler did not finish the culling programs with the entries written out |nit: "On by default on Intel" now reads broader than the code. It is on by default for Intel's Mesa driver (Linux) only, and the Windows driver keeps the written-out form unless
RX_VK_GM_LOOP=1is set.github-sync referenced this pull request2026-11-12 06:22:54 +00:00