Skip to content

fix(shader): route a loop's back edge through an empty continue block when its latch holds instructions - #9746

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
imellag:fix/shader-empty-loop-continue
Oct 24, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
imellag:fix/shader-empty-loop-continue

Conversation

@imellag

@imellag imellag commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

What

On macOS, Stray (PPSA02100) loses the device about 30 s in: Metal reports kIOGPUCommandBufferCallbackErrorHang for the command buffer holding one draw, whose pixel shader (0x8011160000) is a uniform blur loop. Its guest code is simple: two s_buffer_load_dwords read a count (1020) and a step, and the loop runs count >> 1 = 510 times, sampling one texel and adding it per iteration. The test is at the top of the loop and the work, with s_add_u32 s21, s21, 1, is in the latch.

The SPIR-V we emit for it is valid (spirv-val passes) and correct: the header's OpPhis take the incremented counter from the back edge. But the latch is the loop's continue target, and the image sample in it makes the emitter split it into six blocks, so the continue construct is %101 → %485 → %508 → %509 → %510 → %483 and the back edge leaves from %483, not from the continue target. SPIRV-Cross turns that construct into a for (...; ...; increment) expression and drops the header's OpPhi updates from %483:

for (uint _452 = _249, _453 = 0u; !(!(_453 < _440)); _458 = _453 + 1u, _461 = ..., _1135 = ...)
{
}

_453 never changes, so the loop never ends on Metal and the GPU watchdog stops it. The SPIRV-Cross in Vulkan SDK 1.4.363.0 and current SPIRV-Cross main (300681b) both do this; Linux and Windows have no SPIRV-Cross in the path.

Structurizer::canonicalizeNaturalLoops already routes the back edge through a dedicated empty continue block for loops with several latches, a latch in an inner loop, and single-block loops that hold instructions (9d8ee3d). This adds one more case: a single latch that holds instructions and has exactly one predecessor. Its instructions then stay in the loop body, and the continue construct is the empty block alone, which branches to the header. A latch that several continue paths share is left as it is: isolating it would clone its instructions into each path (the four "shared continue" programs in agc_shader_control_flow would each gain 2 to 4 cloned instructions). That case is a new entry in docs/dev/debt/shader.md.

Tested

macOS (Apple M4 Pro, macOS 27.0.1, MoltenVK 1.4.2, Vulkan SDK 1.4.363.0, x86-64 Release under Rosetta 2). main does not build on macOS, so this ran on main + #8658 + #9193 + #9263 + #8612 + #8477 + #9356 + #9387 (with the three fixes from its review) + #9737, with this commit.

  • agc_shader_control_flow, agc_shader_control_flow_iteration_selection_off and agc_shader_control_flow_forced_fallback pass. New program "memory access in a loop latch behind a tested header" (the shape of Stray's shader) runs through the existing structural, execution-equivalence and recompile checks, and the new verifyEmptyLoopContinues requires its continue block, and the existing single-block loop's, to hold no instructions.
  • Negative control: with Structurizer.cpp from main, agc_shader_control_flow fails with a loop whose latch stores behind a tested header: the continue block 3 of the loop at block 1 holds instructions.
  • ctest -R '^(agc|shader|request_serializer)' with ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation: 836 of 858 pass. The same 22 fail on the same stack without this commit, each with the same message, and the validation layer prints the same messages in both runs. They are MoltenVK limits (clip and cull distances, provoking vertex last, sampler reduction, multisample storage, tessellation, interlock) and two serialization tests that fail only because this local stack renumbers the request serialization versions of feat(recompiler): read I/J from barycentric built-ins on MoltenVK #9193, perf(shader): emit exact f32 add, subtract and multiply as fma on MoltenVK #8612 and feat(agc): run mesh-path draws through a compute pass on devices without mesh shaders #9356.
  • Stray: without this change it hangs at that draw every run (also with APS5_SYNC_DRAWS=1 APS5_SYNC_DISPATCH=1, where the command buffer that "Caused GPU Hang Error" holds vertex 0x80122b0000 and pixel 0x8011160000); skipping only that pixel shader (APS5_SKIP_SHADER=8011160000) avoids it. With this change and nothing skipped, it presents frames for the whole 200 s run and draws the first-boot brightness screen correctly.

Linux (Ubuntu 24.04 x86-64 container, GCC 14.2, Release, lavapipe from Mesa 25.2.8), on main + this commit, same environment variables: agc_shader_control_flow, agc_shader_control_flow_iteration_selection_off, agc_shader_control_flow_forced_fallback and agc_shader_memory pass, on main 6a6a35fdf5 and on this PR rebased onto it. These four tests load no Vulkan loader, so the validation layer does not run for them.

NVIDIA (run by @Lindamea1): RTX 5090, driver 595.99.02, Gentoo, GCC 16.2.1, Release, layer 1.4.350, head 8a1ae6b268 merged onto main 8499ea52e, same environment variables: the three agc_shader_control_flow tests pass on both; ^(agc|shader|request_serializer) (874 tests, 554 of which open the GPU and recompile their shaders with no cache) gives the same passes, failures and skip on main and with the change, with the same 25 validation errors in the same tests; the full ctest without the layer (1834 tests) matches main.

tools/check_conventions.py --base upstream/main passes. tools/pr_overlap.py finds no open pull request that changes Structurizer.cpp or tests/ControlFlow.cpp.

Checklist

@github-actions github-actions Bot added the unblocks title Names a title ID on the Unblocks line label Oct 22, 2026
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 5021 docs/dev/debt/shader.md
PR 5371 docs/dev/debt/shader.md
PR 6235 docs/dev/debt/shader.md
PR 6280 docs/dev/debt/shader.md
PR 6412 docs/dev/debt/shader.md
PR 6488 docs/dev/debt/shader.md
PR 6612 docs/dev/debt/shader.md
PR 6924 docs/dev/debt/shader.md
PR 7223 docs/dev/debt/shader.md
PR 7410 docs/dev/debt/shader.md
PR 7484 docs/dev/debt/shader.md
PR 7684 docs/dev/debt/shader.md
PR 8004 docs/dev/debt/shader.md
PR 8077 docs/dev/debt/shader.md
PR 9889 docs/dev/debt/shader.md
PR 9969 docs/dev/debt/shader.md
PR 10233 docs/dev/debt/shader.md

@Dextroid17

Copy link
Copy Markdown
Contributor

Linux data point for b425f0fa8 merged with main b7d1d47a5 (your base 8b3a6ba88 is 50 commits behind it; local merge 97bd08b38, clean): CachyOS, GCC 16.2.1, CMake 4.4.3, Ninja, Release, ANYPS5_NO_SHADER_CACHE=1, ANYPS5_REQUIRE_VULKAN=1, lavapipe ICD set, -j1. CI has not built this PR (every build job on run 39774550493 shows skipping), so beyond the Ubuntu 24.04 / GCC 14.2 run in your body this is the only other Linux build I can see; it adds a second toolchain and the negative control run six times.

Head. ^agc_shader_control_flow|^agc_shader_memory$ (4 tests: agc_shader_control_flow, _iteration_selection_off, _forced_fallback, agc_shader_memory) pass 6 of 6 runs; the ^agc_shader sweep passes 31 of 31, twice (two earlier sweeps, before I had built agc_shader_fusion_tests and agc_shader_helpers_tests, had those two Not Run and 29 passed).

Negative control. core/shader/recompiler/ControlFlow/src/Structurizer.cpp from b7d1d47a5, the PR's core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp kept, rebuilt: agc_shader_control_flow and agc_shader_control_flow_iteration_selection_off fail 6 of 6, always a loop whose latch stores behind a tested header: the continue block 3 of the loop at block 1 holds instructions; _forced_fallback and agc_shader_memory still pass, so the check that fails on main is on the natural-loop canonicalization path your diff edits, and under APS5_FORCE_STRUCTURIZER_FALLBACK=1 the same check passes with either Structurizer.cpp.

agc_shader_control_flow_tests and agc_shader_memory_tests load no Vulkan loader (LD_DEBUG=libs lists libc, libstdc++, libm, libgcc_s and libc.prx, no libvulkan.so.1), so these are recompiler-only results, and "no validation messages" for these four tests cannot come from the layer on any host.

AI-assisted: yes

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reported regression test is well targeted: it includes the tested-header/latch-memory-access shape, checks that the structurizer produces an empty continue block, and the PR reports a negative control against the prior Structurizer. The reported validation-layer suite and title reproduction are useful evidence. I did not find a defect in the one-line structurizer change on this review.

Please refresh this branch from current main before merge and rerun the focused control-flow tests on that base and the refreshed change. The PR head is currently 50 commits behind main (base/head ancestry checked against current upstream/main). CONTRIBUTING.md requires keeping branches current and, for shader recompiler changes, running the same focused tests on main and the change with VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation, ANYPS5_NO_SHADER_CACHE=1, and ANYPS5_REQUIRE_VULKAN=1 via ctest -V, comparing validation output and naming the GPU. The PR records strong runs on its earlier stack and a Linux main + this commit run, but does not establish that the focused paired comparison includes today’s main.

Local checks: python tools/check_conventions.py --base upstream/main --head upstream/pr/9746 passed; git diff --check upstream/main...upstream/pr/9746 passed. The local checkout has no configured build and only about 580 MB free, so I could not independently run the C++/Vulkan tests here. Hosted product build jobs are skipped; conventions, eligibility, intake, labels, and report jobs pass.

@imellag
imellag force-pushed the fix/shader-empty-loop-continue branch from b425f0f to 8a1ae6b Compare October 22, 2026 15:35
@imellag

imellag commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main b7d1d47a5f, now 8a1ae6b268 (clean, no conflicts; check_conventions.py --base upstream/main passes).

Paired run on that base, same container and device for both sides: Ubuntu 24.04 x86-64, GCC 14.2, Release, lavapipe (Mesa 25.2.8), ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation, ctest -R '^(agc_shader_control_flow.*|agc_shader_memory)$' -V:

main b7d1d47a5f this PR 8a1ae6b268
agc_shader_control_flow pass pass (includes the new program and verifyEmptyLoopContinues)
agc_shader_control_flow_iteration_selection_off pass pass
agc_shader_control_flow_forced_fallback pass pass
agc_shader_memory pass pass
was not found but was requested / VUID- lines 0 / 0 0 / 0

@Dextroid17 thank you for the second toolchain and the six negative-control runs. You are right about the validation part: these four tests load no Vulkan loader, so "no validation messages" says nothing for them, and I have removed that claim from the description. The GPU-side evidence for this change is the Stray run on MoltenVK, where the shader that hung now runs. A paired main run on macOS is not possible because main does not build there yet.

AI-assisted: yes

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The author’s follow-up provides the paired control-flow runs on main b7d1d47a5f and the rebased change, with the same focused tests and environment; it also explicitly notes those tests do not load Vulkan. The broader macOS validation-layer run and 200-second Stray reproduction provide useful additional evidence, with the same 22 known failures on both sides and no new layer messages. The exact head 8a1ae6b26806acab44cdff4db36b14ffac7ed71c has a clean merge tree and diff-check against current main 11f976b27c16ed91633f00039dba5dfd97dd7cc1, but is still 21 commits behind. Please refresh the branch and rerun the paired focused control-flow tests on current main and the final head. I did not independently rerun the GPU tests locally.

@imellag
imellag force-pushed the fix/shader-empty-loop-continue branch from 8a1ae6b to 3f8dc18 Compare October 22, 2026 17:35
@imellag

imellag commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main 6a6a35fdf5, now 3f8dc188af. The rebase was clean, and none of the 37 new commits touch the three files this PR changes.

Paired run on that base, same container and device for both sides (Ubuntu 24.04 x86-64, GCC 14.2, Release, lavapipe from Mesa 25.2.8, validation layer 1.3.275, ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation, ctest -V):

main 6a6a35fdf5 this PR 3f8dc188af
agc_shader_control_flow, agc_shader_control_flow_iteration_selection_off, agc_shader_control_flow_forced_fallback, agc_shader_memory pass pass
was not found but was requested / VUID- from these four tests 0 / 0 0 / 0

AI-assisted: yes

@Lindamea1

Copy link
Copy Markdown
Contributor

NVIDIA run on Linux, with the validation layer over the GPU tests: head 8a1ae6b268 merged onto main 8499ea52e (clean). Your current head 3f8dc188af has the same patch on a newer main; I didn't rerun it. RTX 5090, NVIDIA 595.99.02, Gentoo (kernel 7.2.8), GCC 16.2.1, Release. Both runs used ctest -V with ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation (layer 1.4.350). No log has was not found but was requested by env var VK_INSTANCE_LAYERS.

Tests main main + this PR
^agc_shader_control_flow (3) pass pass
^(agc|shader|request_serializer) (874) 870 pass, 3 fail, 1 skipped the same tests pass, fail and skip
validation errors in the 874 25 the same 25, in the same tests
  • As you noted, the control-flow tests don't load Vulkan. The 874-test set is the GPU side: 554 of its tests open the RTX 5090, and with no shader cache they recompile their shaders with the change and run them. Nothing changes. I didn't count how many of those shaders have a loop the change rewrites.
  • agc_shader_memory, the fourth test of your focused set, is among the 874 and passes on both.
  • The 3 failures are agc_driver_sample_order, _grid_reference and _recipe_depth_locations, which fail on main on this GPU (test(agc): read the sample order tests' depth through a sampled copy #9056 and fix(agc): transition both aspects of combined depth/stencil images an… #9807 address them). The skip is agc_driver_stencil_ref_export (no VK_EXT_shader_stencil_export).
  • Full ctest without the layer (1834 tests): same results as main.

AI-assisted: yes (Claude Code). The runs are from this machine.

@imellag

imellag commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

@Lindamea1 thank you for the full GPU sweep: 554 tests recompiling their shaders on the RTX 5090 with the change, with the same results and the same 25 validation errors as main. That covers the GPU side the four control-flow tests could not. I've added it to the description, credited to you.

AI-assisted: yes

@imellag
imellag requested a review from MarcZX10 October 22, 2026 18:04
@MarcZX10

Copy link
Copy Markdown
Contributor

Thanks for refreshing the paired control-flow runs onto 6a6a35fdf5. Current upstream/main has since advanced to caa089fd735d0a5d615279e0f567cb638ad65690; this head is 212 commits behind it. I compared the changed paths against the intervening main history and found no file overlap, which supports the earlier code review, but the repository still requires topic branches to stay current. Please sync the branch before merge and retain the focused control-flow/validation results for the final head. I did not rerun Vulkan tests locally; hosted product jobs remain skipped.

@github-actions github-actions Bot added shader core/shader docs agc libSceAgc* labels Oct 22, 2026
@imellag
imellag force-pushed the fix/shader-empty-loop-continue branch from 3f8dc18 to 75fdf64 Compare October 23, 2026 05:27
@imellag

imellag commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main 9d6adeb550, now 75fdf64721. The rebase was clean, and none of the new commits touch the three files this PR changes; check_conventions.py --base upstream/main passes and pr_overlap.py reports no overlapping PR.

Paired run on that base, same container and device for both sides (Ubuntu 24.04 x86-64, GCC 14.2, Release, lavapipe from Mesa 25.2.8, validation layer 1.3.275, ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation, ctest -V):

main 9d6adeb550 this PR 75fdf64721
agc_shader_control_flow, agc_shader_control_flow_iteration_selection_off, agc_shader_control_flow_forced_fallback, agc_shader_memory pass pass
request_serializer, agc_driver_graphics, agc_driver_depth_bounds_state, agc_driver_pixel_barycentric_emulation pass pass
was not found but was requested 0 0
VUID- VUID-VkDeviceCreateInfo-pNext-pNext, once the same, once

The one validation message is the same on both sides: Ubuntu's 1.3.275 layer prints it at device creation because it does not know one structure in the device's pNext chain.

AI-assisted: yes

@iamthehimansh

Copy link
Copy Markdown
Contributor

Tested on an Apple M2 Pro, macOS, x86_64 Release under Rosetta 2, MoltenVK 1.4.2, applied on top of #8658's feat/macos-port (27abf7e4b) together with #8477, #9263, #9746 and #9406. I built the whole tree and ran the full suite (1,962 tests), then ran any new failure 3x on its own with each PR alone.

AI-assisted: yes (Cana)

@adriadam10 adriadam10 self-assigned this Oct 23, 2026
@adriadam10
adriadam10 dismissed MarcZX10’s stale review October 24, 2026 10:20

Dismissing a stale review: the requested changes were addressed after it (new head and/or updated description with the asked-for runs) and the merge queue blocks on it. Please request changes again if anything is still open.

@adriadam10
adriadam10 merged commit 18e8539 into boykopovar:main Oct 24, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* docs shader core/shader unblocks title Names a title ID on the Unblocks line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants