Skip to content

feat(recompiler): structure every control-flow graph the structurizer refuses - #4926

Merged
oneandonlydean merged 2 commits into
boykopovar:mainfrom
nikowolf123:feat/structurizer-complete-fallback
Oct 14, 2026
Merged

oneandonlydean merged 2 commits into
boykopovar:mainfrom
nikowolf123:feat/structurizer-complete-fallback

Conversation

@nikowolf123

@nikowolf123 nikowolf123 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

What

A structurizer refusal stops the title: Recompile memoizes it per source and the dispatch or draw that needs the shader turns it into a sticky driver failure. The structurizer has grown a ladder of retries (two attempt series of up to four configurations) that each refuse some shapes. Measured on main with the oracle described under Tested, on 200,000 random graphs of 2 to 32 blocks: of the 82,743 reducible graphs whose every block reaches a return, 3,041 (3.7%) are refused. Irreducible graphs and graphs with a cycle that never exits are refused by design (114,667 in that corpus).

This PR adds a construction that structures every graph built from branch, conditional branch and return blocks, and uses it only when the ladder refuses:

  • The graph is decomposed into strongly connected components, recursively: a component's head is its member entered from outside (or the entry), and the rest is decomposed again. Components are emitted in topological order.
  • Each original block keeps exactly one copy and runs behind a guard on its own reach flag (a route variable, as the ladder already uses). Every edge of a block sets the reach flag of its target. Each cyclic component becomes a loop whose single latch repeats it while a back edge set its again flag; an edge out of a loop just sets its target's flag, and the loop's latch exits.
  • Exactly one reach flag is set at any time and it names the next original block of the input execution, so the original blocks run in the same order and each conditional consumes the same decision as in the input, for every branch decision. No block is cloned, so a block with s_barrier keeps one copy.
  • The output uses only selections with a merge, loops with one latch and returns. An empty return ends the function so that every block reaches one; it never runs.
  • Blocks that start the top-level order unconditionally run unguarded. Otherwise SSA would see a static path around the entry block and merge its results with initial values.
  • The ladder runs first, unchanged, and its output is unchanged. If the construction itself fails, the ladder's error is reported as before. APS5_FORCE_STRUCTURIZER_FALLBACK=1 uses the construction for every graph; it is part of the shader cache key.

This is the textbook state-variable construction (Böhm and Jacopini; Ashcroft and Manna) applied to the structurizer's own route variables. It costs about 8 times the input's blocks and one variable per block, which the technical debt entry records with the other limits below.

Limits:

  • Time is roughly quadratic in the block count, from the existing analyses over the larger output: 4.8 ms mean at up to 200 blocks, 0.45 s mean at 500 to 1000.
  • The ladder still runs first. On 1,149 of the 200,000 graphs (1,220 on main) it spends more than 10 s before it accepts or refuses, and this PR does not shorten that.
  • Guarded blocks give resource planning static paths that skip them, so it can plan extra descriptor candidates on graphs that use the fallback.

No open PR changes Structurizer.cpp. #2762 concerns ladder outputs that are invalid SPIR-V; this PR does not change ladder outputs.

Tested

WSL2 Ubuntu 26.04, GCC 15.2.0, configured like CI (Release, SPIRV-Tools ON, lavapipe), and native Windows 11 with WinLibs GCC 15.2.0 on an RTX 5080 (NVIDIA 616.92). No title.

In-tree:

  • New cases in agc_structurizer structure eight graphs that main refuses (a duplicate merge, a region that wraps around to its dominator, the merge-splitting budget, a conditional without a structured merge, an irreducible entry and three loops that never exit) and the Structurizer: shared merge splitting never terminates for nested selections in a two-latch loop #2642 two-latch loop, which main accepts. Each must give structured branches, every original block exactly once and the same executions as the input for every branch decision.
  • New agc_structurizer_forced_fallback and agc_shader_control_flow_forced_fallback run the in-tree graphs and the 35 RDNA programs through the fallback, through Recompile and SPIR-V validation before and after spirv-opt, for 32 and 64 lanes.
  • Full ctest in the CI configuration on main e51428b: 1014 tests, 0 failed. tools/check_skips.py flags only guest_video_out_high_frame_rate, which skips because the WSL display has no 120 Hz mode, as it does on main.
  • Native full ctest (ANYPS5_REQUIRE_VULKAN=1): 1030 tests, 0 failed, 7 skipped as tools/check_skips.py --job windows expects.
  • The same native suite with APS5_FORCE_STRUCTURIZER_FALLBACK=1 ANYPS5_NO_SHADER_CACHE=1, so that every shader of every GPU execution test is structured by the fallback and runs on the RTX 5080: 1028 of 1030 pass. The two failures check analysis output, not GPU results. agc_shader_memory expects a descriptor phi with two candidates and gets a third from a guarded path that never runs (the planning limit above). agc_scalar_select_mask runs programs through its own evaluator, which does not model route variables.

Out of tree, with an oracle that shares no code with the structurizer (outside the repository; I can attach it). For every output it checks the graph contract (dense ids, exact successors and predecessors, reachability, dominator order, one latch per loop, unique merges, synthetic block shapes), runs SPIRV-Tools on a skeleton module emitted exactly as SpirvFlowEmitter lays out blocks and merges, at SPIR-V 1.3, 1.4 and 1.6, and decides semantic equivalence exactly: a search over pairs of (input block, output block and live route-variable values) that compares the original block reached next for every branch decision, loops included. It was checked against 593,159 mutated outputs and an independent simulator, with no missed difference.

  • main, 200,000 graphs: every one of the 81,072 accepted graphs is valid at 1.3 and 1.4 and equivalent; 30,220 of them contain a conditional with equal targets, which only 1.6 rejects (the fix(shader): never branch conditionally to one label twice #4580 topic). 117,708 graphs are refused, 3,041 of them reducible and terminating, and 1,220 run past 10 s.
  • This PR, same 200,000 graphs: the 81,072 graphs main accepts give byte-identical outputs. Every graph main refuses (117,708) gives a valid, equivalent output. No graph is refused.
  • Every graph forced through the fallback: 2,800,000 graphs over three seeds (up to 64 blocks), all valid at 1.3, 1.4 and 1.6 and equivalent, including irreducible and non-terminating graphs; 20,000 of them also under AddressSanitizer and UBSan. The only rule the outputs break is the oracle's own list of synthetic block shapes, which did not include the final empty return.
  • Rebased onto main cac4e96: full ctest in the CI configuration, with this PR and its follow-up on top, 1032 tests, 0 failed. Then onto 4bb2c7f, where only the technical debt file conflicted: the structurizer, control-flow and shader tests, 18 of 18.

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
  • GPUs tested: lavapipe (Mesa 26.0.8, llvmpipe) on WSL2 Ubuntu 26.04; NVIDIA GeForce RTX 5080, driver 616.92, Windows 11
  • AI-assisted: yes. Written with Claude Code; I reviewed every line and ran the tests above.

🤖 Generated with Claude Code

@github-actions github-actions Bot added build shader core/shader docs agc libSceAgc* labels Oct 13, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 13, 2026
@nikowolf123
nikowolf123 force-pushed the feat/structurizer-complete-fallback branch from 8881609 to 504acb1 Compare October 13, 2026 08:53
@nikowolf123
nikowolf123 force-pushed the feat/structurizer-complete-fallback branch from 504acb1 to 3acb962 Compare October 13, 2026 08:58
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 13, 2026
@nikowolf123
nikowolf123 force-pushed the feat/structurizer-complete-fallback branch 2 times, most recently from e59b624 to 395280c Compare October 13, 2026 16:21
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 13, 2026
nikowolf123 and others added 2 commits October 14, 2026 00:59
When every attempt of the structurizer ladder refuses a graph, the title
aborts. Rebuild such a graph from a hierarchical decomposition into
strongly connected components instead: each original block keeps one copy
and runs behind a guard on its own reach flag, and each component becomes
a loop with a single latch that repeats it while a back edge set its again
flag. Every edge sets the reach flag of its target, so exactly one flag
names the next original block and the blocks run in the input's order for
every branch decision. Irreducible and non-terminating graphs are handled
the same way, and no block is cloned.

The ladder runs first and its output is unchanged. The fallback only runs
when both attempt series refuse; if it cannot structure the graph, the
ladder's error is reported as before. Blocks that start the top-level
order unconditionally run unguarded, so the entry block's results reach
later blocks without a stale path. APS5_FORCE_STRUCTURIZER_FALLBACK=1
uses the construction for every supported graph, for tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…zer fallback

Structurize eight graphs the ladder refuses on main (a duplicate merge, a
region that wraps around to its dominator, the merge-splitting budget, a
conditional without a structured merge, an irreducible entry and three
loops that never exit) and the boykopovar#2642 two-latch loop, which the ladder
accepts. Each must give structured branches, every original block exactly
once and the same executions as the input for every branch decision.

Register agc_structurizer_forced_fallback and
agc_shader_control_flow_forced_fallback, which run the in-tree graphs and
RDNA programs, through Recompile and SPIR-V validation, with
APS5_FORCE_STRUCTURIZER_FALLBACK=1. Under that switch the split-kind
checks become checks that no block is cloned and a route variable is
used, and the nested-selection check names blocks by their original
instructions instead of their position.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nikowolf123
nikowolf123 force-pushed the feat/structurizer-complete-fallback branch from 395280c to 22c0c51 Compare October 13, 2026 23:07
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 13, 2026
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
#4806 docs/dev/debt/shader.md L117
#5049 docs/dev/debt/shader.md L117
#5461 docs/dev/debt/shader.md L117
#5472 docs/dev/debt/shader.md L117

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Closing: no title is known to reach this. With the queue at 700+ open PRs, please hold off on new PRs that don't name a title needing the change until it's down. If a title stops on this on current main, reopen with the title ID and the log line.

@oneandonlydean oneandonlydean self-assigned this Oct 14, 2026
@oneandonlydean
oneandonlydean merged commit eac1e26 into boykopovar:main Oct 14, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* build docs shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants