Skip to content

fix(recompiler): take a paired wave64 zero branch when some invocations are inactive - #12211

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
XairRrR:fix/shader-paired-wave-zero-branch
Oct 28, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
XairRrR:fix/shader-paired-wave-zero-branch

Conversation

@XairRrR

@XairRrR XairRrR commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

What

A wave64 program held as 32 invocations x 2 lanes decides s_cbranch_execz, s_cbranch_vccz and s_cbranch_scc0 in EmitBranchCondition by balloting the per-lane "zero" condition and requiring all 64 bits set. OpGroupNonUniformBallot sets the bit of an inactive invocation to 0 (SPIR-V specification, OpGroupNonUniformBallot: "The bit is set to one if the corresponding invocation is active and the Predicate for that invocation evaluated to true; otherwise, it is set to zero"), and an invocation that has returned from the entry point is no longer active. Once one invocation stopped, for example after a BDA fault, the other invocations never took the zero branch, so a loop that leaves through s_cbranch_execz never ended.

The RDNA2 ISA takes these branches when the whole register is zero (SOPP S_CBRANCH_EXECZ / VCCZ / SCC0: "if (EXEC == 0)", "if (VCC == 0)", "if (SCC == 0)"). The branch now asks whether any active invocation still has a lane with the bit set, as the wave32 path does: not any(not own || not other) over the lane pair's zero conditions, so inactive invocations count as lanes whose bit is clear. The non-zero kinds keep any(own || other).

Found while hunting a GPU hang in PPSA05684 (Uncharted: Legacy of Thieves Collection): its 320x180 light-loop compute program is wave64 and leaves its loops through s_cbranch_execz. The hang itself turned out to be the mip-chain issue that #6280 (open) fixes; this branch bug is real on its own and has a test.

Tested

Arch Linux 7.2.11-zen, GCC 16.2.1, Ninja, Release, on main 3ca715e:

  • New agc_driver_paired_wave_zero_branches: a wave64 program in which lane 0 faults and stops its invocation, then takes each zero and non-zero branch and records which way each lane went. With SpirvFlowEmitter.cpp from main it fails: lane 1 fell through s_cbranch_execz after another invocation of its wave stopped.
  • Full ctest -j8 (this branch together with my five other branches of today, on the same main): 2619 of 2620 pass; agc_driver_mesh_barycentric_emulation fails the same way on main on this GPU (the pixel shader did not emulate barycentrics).
  • tools/check_conventions.py --base origin/main passes.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Fixes: none
  • Unblocks: none
  • GPUs tested: NVIDIA GeForce RTX 4070 Ti SUPER, driver 615.78.08, Arch Linux 7.2.11-zen
  • AI-assisted: yes

…ns are inactive

A wave64 program held as 32 invocations x 2 lanes decided s_cbranch_execz,
s_cbranch_vccz and s_cbranch_scc0 by balloting the per-lane "zero"
condition and requiring all 64 bits set. OpGroupNonUniformBallot sets the
bit of an inactive invocation to 0 (SPIR-V specification,
OpGroupNonUniformBallot: "The bit is set to one if the corresponding
invocation is active and the Predicate for that invocation evaluated to
true; otherwise, it is set to zero"), and an invocation that returned
from the entry point is no longer active. When one invocation stopped,
e.g. after a BDA fault, the zero branch was never taken for the others:
a loop leaving through s_cbranch_execz never ended.

The RDNA2 ISA branches when the whole register is zero ("if (EXEC == 0)",
"if (VCC == 0)", "if (SCC == 0)", SOPP S_CBRANCH_EXECZ / VCCZ / SCC0), so
the branch now asks whether any active invocation still has a lane with
the bit set, as the wave32 path does: not any(own lane set || other
lane set). Inactive invocations then count as lanes with the bit clear.
The non-zero kinds keep any(own || other).

The execution test runs a wave64 program in which lane 0 faults and
stops its invocation, then takes each zero and non-zero branch; before
this change the other lanes fell through all three zero branches.

AI-assisted: yes
@github-actions

github-actions Bot commented Oct 28, 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 core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp
PR 7410 core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp
PR 7642 core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp
PR 9508 core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp
PR 10193 core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp
PR 11265 core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp

@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 paired condition logic is consistent with the SPIR-V ballot rule cited in the PR, and the focused regression plus same-GPU main comparison are useful evidence. I found no independent defect in the change or test by static inspection. This branch is 56 commits behind current main, however, and the contribution rules require an up-to-date branch. Please rebase on current main, rerun the paired-wave test and the same focused validation run on main and the rebased change, then I can reassess.

@MarcZX10

Copy link
Copy Markdown
Contributor

Correction to my review: I re-counted against the refreshed main tip; this branch is 60 commits behind, not 56 as stated in the review. The requested rebase and focused rerun still apply.

@github-actions github-actions Bot added shader core/shader agc libSceAgc* labels Oct 28, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

@XairRrR The fix looks right to me. Thanks for the clear write-up and the spec citation.

One optional request on the test. agc_driver_paired_wave_zero_branches only exercises the fix when the wave64 compute program is compiled in the paired form (32 invocations x 2 lanes, host subgroup size 32). On a device that doesn't give it a 32-wide subgroup, such as lavapipe, it takes the single-lane path and passes without reaching the changed code. Could the test confirm that the recompile used the paired form, and return VulkanTestSkipped when it didn't? That way a pass always means the fixed path ran. This isn't blocking.

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

Labels

agc libSceAgc* shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants