Skip to content

perf(shader): judge first-lane and constant reads by what they read - #6053

Merged
oneandonlydean merged 2 commits into
boykopovar:mainfrom
ThatsLeo:perf/shader-plan-first-lane
Oct 17, 2026
Merged

oneandonlydean merged 2 commits into
boykopovar:mainfrom
ThatsLeo:perf/shader-plan-first-lane

Conversation

@ThatsLeo

@ThatsLeo ThatsLeo commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

What

Since #5181, a resource plan keeps a control-flow condition only when Detail::Evaluator::MayEvaluate finds that the evaluator could compute it. As @AnthonyClemens found on Resident Evil 2 (#5181), MayEvaluate took ReadFirstLane and ReadConst as evaluable without looking at what they read. A condition behind either was kept with everything it depends on, though the evaluator could not compute it.

MayEvaluate now judges both the way the evaluator reads them:

  • ReadFirstLane is evaluable when its source is, judged under the read's active mask. Inside it, a runtime select on that mask needs only its first arm, which is the arm the evaluator takes there.
  • ReadConst is evaluable when the SRT read its slot names is. A slot that is not an immediate below the number of SRT reads is not evaluable, as in the evaluator.
  • Cycles: a value that reaches itself counts as not evaluable. The evaluator's visiting list stops such a value the same way.

#6095 compiles these walks and treats both opcodes the same way (a first-lane read under its own mask, a constant read through its slot), so the rule matches it too.

The rule is one way only. A value the evaluator can compute is never judged not evaluable, so no condition the walk could use is dropped. A value judged evaluable may still fail at run time (for example a ReadConst answered by the clean-memory evaluator, which has no active mask); that costs memory, as before.

Tested

Fedora 44, GCC 16.2.1, Release, Intel Arc (Meteor Lake iGPU, Mesa 26.2.3 ANV).

  • verifyMayEvaluate (agc_shader_memory) has eight new cases, each checked against the evaluator itself:

    • first-lane reads of user data and of a vector register;
    • first-lane reads of a select on the active mask, with user data or a vector register in the active arm;
    • constant reads whose slot holds user data or a vector register;
    • a constant read past the last slot, and one whose slot reads itself;
    • a first-lane read of a loop-carried value that reaches itself through the read, and a constant read whose slot reads itself through a first-lane read. Each first-lane read starts a new walk under its own mask, so before the second commit both MayEvaluate and the evaluator recursed until the stack overflowed on these. Both now track the first-lane reads they are inside and judge such a read not evaluable.

    ReadFirstLane and ReadConst also left the list of opcodes the over-constants sweep skips.

  • ctest (1360 tests) on Arc and on lavapipe, rebased onto main 47a266cf9. All pass except two that don't involve this change:

    agc_shader_memory also passes with APS5_VERIFY_SRT_COMPACT=1. On earlier bases, agc_driver_preparation_scope_settle failed sometimes on Arc. It is flaky without this change: run alone ten times, it failed 6 times on this branch, 8 on perf(shader): keep only evaluable branch conditions in resource plans #5181's head and 5 on main 19fc5a8ed.

  • Plan sizes, Stray (PPSA02101) and Dreaming Sarah (PPSA02929): I extracted the plans of the 227 distinct shader requests I have dumped from both titles (ExtractPlan), with and without this commit. They hold the same 36,699 values either way: these plans contain no ReadFirstLane, and their 4,594 ReadConsts all read slots that can be evaluated. So on these two titles the commit changes nothing. I have no title where the case occurs.

  • @AnthonyClemens measured 5.6 GB down to about 0.43 GB of decoded plans on Resident Evil 2, but with a pass that differs from this one and with both opcodes changed at once. A run of this branch with a warm cache would give the number for this rule, and show how the saving splits between the two opcodes.

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
  • Unblocks: none on its own
  • GPUs tested: Intel Arc (Meteor Lake iGPU), Mesa 26.2.3 ANV, Fedora 44; lavapipe (Mesa 26.2.3, LLVM 22)
  • AI-assisted: yes

@github-actions github-actions Bot added shader core/shader docs agc libSceAgc* labels Oct 14, 2026
@oneandonlydean oneandonlydean self-assigned this Oct 14, 2026
@ThatsLeo
ThatsLeo force-pushed the perf/shader-plan-first-lane branch from f8a0809 to 7b47325 Compare October 14, 2026 23:46
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 15, 2026
@ThatsLeo
ThatsLeo force-pushed the perf/shader-plan-first-lane branch from 7b47325 to 1d41c15 Compare October 15, 2026 11:29
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 15, 2026
@github-actions

github-actions Bot commented Oct 15, 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 1341 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 4339 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 5371 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 5500 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 5609 core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp
core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp
PR 5925 docs/dev/ARCHITECTURE.md
PR 6044 docs/dev/ARCHITECTURE.md
PR 6177 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6222 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6235 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6412 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
docs/dev/ARCHITECTURE.md
PR 6539 core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp
core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp
PR 6645 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6946 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
docs/dev/ARCHITECTURE.md
PR 6947 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
docs/dev/ARCHITECTURE.md
PR 6973 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6987 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6989 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6995 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 7002 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp

@oneandonlydean

Copy link
Copy Markdown
Collaborator

One case I'd like covered. MayEvaluate catches a cycle through expanding, but a ReadFirstLane starts a nested MayEvaluate with fresh known and expanding sets. A value that reaches itself through a first-lane read is therefore only stopped by ResolveInvariantPhi turning every loop phi into its invariant input. Could you add a test with a loop-carried value read through ReadFirstLane (it should come out not evaluable, without recursing), or state that invariant where MayEvaluate is described, so a later change to phi resolution can't turn it into unbounded recursion?

@ThatsLeo
ThatsLeo force-pushed the perf/shader-plan-first-lane branch from 1d41c15 to e14a4e5 Compare October 17, 2026 14:14
@ThatsLeo

ThatsLeo commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor Author

@oneandonlydean Added in e14a4e5, with the branch rebased onto main 47a266cf9. ResolveInvariantPhi did stop your loop case, but a slot can also reach itself through a first-lane read with no phi in between, and nothing stopped that.

verifyMayEvaluate has two new cases:

  • Your loop case: a phi carrying ReadFirstLane(phi) + 1 around a loop, read through ReadFirstLane.
  • The phi-free case: a ReadConst whose slot holds ReadFirstLane of that same ReadConst.

The second one overflowed the stack, in MayEvaluate and also in the evaluator, which already did the same on main: each first-lane read starts a nested BasicEvaluator with fresh visiting state.

The fix:

  • MayEvaluate passes the set of first-lane reads it is inside down to the nested walk. One already in the set is judged not evaluable.
  • The evaluator keeps the same chain through the nested evaluators, and returns false for a read it is already evaluating.
  • Both cases come out not evaluable, in MayEvaluate and in Evaluate.

The invariant is now stated in the plan section of ARCHITECTURE.md. ctest on Arc and lavapipe matches main (details in the description).

AI-assisted: yes

MayEvaluate took ReadFirstLane and ReadConst as evaluable without looking at
their sources, so plans kept conditions behind per-lane values.
MayEvaluate and the evaluator track the first-lane reads they are inside, so a read reached again through a loop-carried phi or a constant slot is not evaluable instead of recursing.
@oneandonlydean
oneandonlydean merged commit fb20d5d into boykopovar:main Oct 17, 2026
9 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants