Commit Graph

14 Commits

Author SHA1 Message Date
Celegans12 f6fbb480c5 perf(recompiler): preserve sorted dominance sets during insertion
Both dominance analyses start from sorted unique sets and intersect them.
Insert the current block ID with lower_bound instead of sorting again.
Preserve every intermediate set and the existing ID-order iteration.
Keep the generic unsorted helper and all graph rewrite logic unchanged.
Compare both analyses against the original algorithms on six raw CFGs.
Cover finite loops, closed SCCs, unreachable islands and permuted IDs.
2026-10-10 09:20:14 +02:00
XairRrR 99a96c3df3 fix(recompiler): clone small return blocks shared by early exits
A pixel shader whose early discards branch to one small ending block
gave that block several conditional predecessors outside any loop, and
structurizing failed. When the regular pass throws, the structurizer
restarts from the original graph and first gives each conditional
predecessor its own copy of such a return block (within the clone word
floor and a budget of four clones per block).

Uncharted: Legacy of Thieves Collection (PPSA05684) stopped at such a pixel shader with "conditional block 15 has no structured merge".
2026-10-10 08:41:13 +02:00
Adrià 63cd22f395
Merge pull request #2105 from Raghav314/perf/recompiler-post-dominators-postorder
perf(recompiler): compute post-dominators visiting successors first
2026-10-10 08:13:07 +02:00
raghav314 f57d1647b5 perf(recompiler): compute post-dominators visiting successors first
computePostDominators iterates its sets to a fixed point, visiting the
blocks in id order. Post-dominance flows from a block's successors back to
it, and successors mostly have higher ids, so each pass moved the sets back
by about one block: a long program needed about as many passes as it has
blocks, each costing O(blocks^2). recomputeAnalyses runs it after every
split of splitSharedMergeBlocks, up to four times per block.

Visit the blocks in post-order of the CFG (successors first), then any the
entry does not reach. The sets start from every block and only shrink, so
the iteration still converges to the same greatest fixed point, now in a
few passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:40:08 +05:30
LeandroLP23 81624a6a18 refactor(recompiler): share the control flow helpers
toHexString, contains, intersectSorted, addUnique, sortUnique, remapId,
remapIds and rebuildPredecessors were defined with identical bodies in
the anonymous namespaces of two of ControlFlowGraph.cpp, GraphBuilder.cpp
and Structurizer.cpp. They are now declared in ControlFlowHelpers.hpp,
which only these three files include, and defined in ControlFlowGraph.cpp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:06:31 +02:00
alfalfa-star 0ec3061a46 fix(recompiler): merge a loop selection where its other arm joins or leaves
A selection inside a loop whose arm either joins the other target or
continues/breaks (`if (a) { if (b) continue; x; }`) got the nearest
common post-dominator as its merge. When the loop exits after the outer
join, that is the outer selection's merge, so both selections shared
it. splitSharedMergeBlocks then inserted a forwarding block in front of
it, recomputed the same merge, and repeated until the budget ran out:
PPSA14632's compute shader 0x1a80ee0000 stopped with "CFG shared merge
splitting exceeded budget: originalBlocks=92 currentBlocks=460".

findSelectionMerge now picks the joined target when the other arm is a
conditional block with the header as its only predecessor, and every
successor of that arm is the joined target or the loop's continue or
merge block. The shader structures with 13 added blocks and no clones
or route variables.

In the result the outer selection header dominates the loop's continue
target, so the test's verifyStructured counted the continue target as a
selection member that branches outside (back to the loop header).
SPIR-V uses structural dominance, where the loop header's edge to its
continue target keeps it out of the selection, so verifyStructured now
skips the continue construct of an enclosing loop. spirv-val accepts
the new test's SPIR-V for both subgroup sizes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 15:31:45 +02:00
Celegans12 adee347c14 feat(recompiler): structurize loops whose exits reach different program ends
A loop whose exits have no common post-dominator, such as a normal exit
to one s_endpgm tail beside an early-out to a kill tail, got no merge
block, and Structurize threw "loop at block N has no structured
merge/continue". With three or more exits, detectNaturalLoops threw
first, on a lookup of InvalidControlFlowId.

Such a loop now leaves through one new merge block that dispatches on
route variables. They are cleared before the header and set on each
exit edge to a target other than the latch's exit. Inner loops are
routed first; a routing that would change the natural loops throws.

A loop-exit tail branch whose in-body target is the continue block no
longer merges there, which spirv-val rejects ("Header block ... is
contained in the loop construct ..., but its merge block ... is not").
2026-10-06 00:35:47 +02:00
Dean Galvin d209a2e5c1 fix(recompiler): give an early-return selection that joins its parent's merge a merge of its own
A selection whose one arm only returns (kill or s_endpgm) and whose other
arm branches straight to the merge of an enclosing selection had no
structured merge, so the draw was skipped. PPSA21564's BotEye G-buffer
pixel shaders (eight variants) have this shape.

When no other split applies, the edge from such a header to the shared
join is now split into an empty block, which becomes the header's merge.
2026-10-03 10:01:45 +13:00
Dean Galvin 66ddf59c22 fix(recompiler): give a loop's exit-tail branch a selection merge
A conditional inside a loop with one target in the loop body and the
other outside it was taken for a break and emitted without a merge. When
the outside target is not the loop's merge but a tail of blocks that
reaches it, SPIR-V has no construct for that branch: spirv-val rejects
the module ("Selection must be structured") and the driver is free to
compile it into anything.

Such a branch now merges at its in-body target when the tail is private
to it (every tail block is dominated by the tail's first block, stays in
the loop construct and leaves only for the loop's merge), so the tail is
an "if (c) { tail; break; }" the loop already allows. Merges are given
to these after every other construct's, and one already taken leaves the
branch as before.

All 13 PPSA21564 programs whose V#s are read from GPU memory (the
deferred lighting and ray-traced passes) had two such branches in their
traversal loops. With this change none of the 74 dumped title programs
has one, and the 209 modules of a title-to-cinematic run validate apart
from three unrelated image-offset and entry-point errors.

Tests: agc_structurizer_tests structures a loop that exits through one
tail block and a loop with two exit tails, and requires every merge-less
conditional to target its loop's merge or continue.
2026-10-02 10:23:02 +13:00
Dean Galvin b57ab5a0e9 feat(recompiler): structurize externally entered selection regions by cloning or route variables
A selection whose region (the blocks reachable from its arms before its
merge) is also entered from outside the header cannot be expressed as a
SPIR-V selection construct, and the structurizer gave up with "semantic
block cloning is disabled". Pixel shaders hit this through a shared
discard tail (s_mov_b64 exec, 0 / exp null / s_endpgm) that several
selections branch to, compute shaders through short-circuit conditions
that jump into a shared body, and ray-traced compute passes through an
s_cbranch_execz that skips past a join into the middle of a selection.

Such a region is made single-entry in one of two ways, chosen per region
by its estimated translated size:

- Cloning (node splitting): the blocks the outside edges reach inside the
  region are copied and the outside edges redirected to the copies. When
  the header's merge is a shared linear return tail, giving the header a
  private copy of that tail is tried first. Each block now carries an
  estimate of the SPIR-V words it translates to. The per-instruction
  weights come from a fit of the recompiler's per-block output (before
  spirv-opt, phis excluded) over 8,300 blocks of captured shaders: VALU
  60, buffer and flat 120, SMEM 180, image 220, LDS 400. SALU (8) and
  exports (20) fit near zero and keep a small weight. Lane ops (120) and
  image_bvh_intersect_ray (2000) were too rare there to fit and are
  weighted for the wave and traversal expansions they need. A region is cloned only when it is at most 1024 estimated
  words and at most an eighth of the program (at least 256), and while
  all clones stay within a quarter of the program.
- Routing through a junction, for larger regions: every edge into the
  entered part (from outside, and from the header's own arms) and every
  edge from the arms to the merge is sent through a new empty junction
  block. Each rerouted edge first passes empty blocks that assign
  Boolean route variables (the IR's goto variables); the junction tests
  them in order to reach the edge's original target. The entered part
  then follows the junction and is emitted once. Branch conditions are
  still evaluated only at the end of their original blocks: a route
  variable records the edge a block already took, so a later SCC, VCC or
  EXEC write cannot change the route, and in the two-lane wave64 layout
  the source branch is combined across both halves as before while the
  route variable holds the same constant in both. Routing is refused
  when a rerouted edge is a back edge or leaves an indirect branch, when
  the region wraps around to a dominating block, or when it would change
  the natural loops or make the graph irreducible; cloning bounded by the
  program's estimated size is the fallback, and otherwise it throws with
  the header, the entered block and both reasons.

Selections whose arms only meet again after private return tails had no
post-dominator merge. When the existing rules find none, the merge is
now looked up with edges into private return tails ignored, or taken as
the other arm when one arm only returns; the candidate must still exit
the construct only through the merge. applyBlockOrder also remaps
indirect branch targets.

agc_shader_control_flow_tests (under ctest) covers hand-assembled
gfx1030 programs: the cloned shapes above, and routed ones (an
s_cbranch_execz into an expensive region with a loop, a region inside a
loop, nested entered regions, a shared body reached after SCC is
overwritten, early returns beside an entered region, an arm that only
returns). It checks that every selection construct exits only through
its merge, follows route variables to check that the structured graph
runs the same instruction ranges under the same branch conditions as
the original, bounds the cloned instructions, recompiles each program
for 64- and 32-lane subgroups, and keeps the routed execz program within
10% of the SPIR-V of the same program without the entering branch.
Given .req files it checks their graphs instead. agc_shader_replay --cfg
prints a request's graph, now with each block's estimate, before and
after structurization.
2026-10-01 08:36:11 +13:00
STL1te 6b36c050fe style: drop the branch's code comments per CONVENTIONS.md
Only the lines this branch added lose their comments; the commits before
keep them, with the reasoning behind each change, for whoever digs into
that code later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:25:29 +03:00
STL1te 6a73ec426f fix(shader): a selection whose arm continues merges where the other arm goes
findSelectionMerge took the nearest common post-dominator of a selection's
targets, computed across the loop's back edge. When one arm only continues
(reaching that block in a later iteration) and the other target is shared
with an enclosing selection, the shared merge was split off the header, the
post-dominator stayed where it was, and splitting repeated until the budget
threw.

Inside a loop, an arm that reaches the post-dominator only past the continue
or merge block now leaves the selection by continuing, and the merge is the
other arm's target; after one split that is the header's own synthetic merge.
agc_structurizer_tests builds the smallest such graph; it hit the budget
before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:22:20 +03:00
boykopovar af2ff821c4 feat(shader): shader control flow structurizer impl 2026-09-15 10:01:58 +03:00
boykopovar d084eed394 feat(shader): rdna memory op decoder impl 2026-09-15 07:07:50 +03:00
Renamed from core/shader/recompilier/ControlFlow/src/Structurizer.cpp (Browse further)