Skip to content

refactor(shader): walk each construct when checking the structured constructs - #9703

Merged
adriadam10 merged 11 commits into
boykopovar:mainfrom
frankea:perf/structurizer-construct-walk
Oct 26, 2026
Merged

adriadam10 merged 11 commits into
boykopovar:mainfrom
frankea:perf/structurizer-construct-walk

Conversation

@frankea

@frankea frankea commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

What

hasStructuredConstructs runs after every structurize attempt. For each merge header it tested every block of the graph, and for each block it searched every block for its innermost loop, with dominator binary searches each time. The work grows with the number of headers times the number of blocks. With APS5_FORCE_STRUCTURIZER_FALLBACK=1, Ghostrunner's compute shader 0x200c520000 (PPSA03685, linked with #9068 and #8070) becomes a 32,490-block reach-flag graph with 6,647 merge headers, so the check runs about 216 million member tests.

  • The innermost loop is now searched among the loop headers only. Every other block failed the loop-header test first, so the candidates and their order are unchanged.
  • When constructsFollowDominance (from fix(shader): structurize a 4,750-block graph before Unreal's render thread watchdog fires #9702) holds for the structural graph, the member checks visit only the blocks dominatedBlocks finds from the header, stopping at its merge, and for a loop also from its continue block, in block order. Only a block in the header's construct or in the loop's continue construct can fail a member check. A construct block is dominated by the header and not by the merge, so some path from the entry reaches it without the merge; from the last header on that path every block is dominated by the header, so the walk reaches it. A continue-construct block is dominated by the continue block and is reached from it the same way. The blocks the walk skips passed every check, so the result is unchanged.
  • Otherwise every block is checked as before, and APS5_NO_PERF_DOMINATOR_TREE keeps the old loop. Its row in docs/dev/ENVIRONMENT.md says so.

Tested

  • Refreshed onto main 0db47b2 (head 98c6aef, through fix(shader): structurize a 4,750-block graph before Unreal's render thread watchdog fires #9702's refreshed head; no conflicts), RTX 4090 (NVIDIA 616.92, Windows 11): the 11 ctest entries of agc_structurizer, agc_shader_control_flow, scalar_call and agc_driver_compute_table_calls pass 11/11, each test run three times, and 11/11 under the validation layer (VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation, ANYPS5_NO_SHADER_CACHE=1, ANYPS5_REQUIRE_VULKAN=1, ctest -V), with no VUID- line; the layer reports nothing on this head. main 0db47b2 passes the union of the refreshed sets, 115/115, under the same layer settings with no VUID- line.
    This PR makes no speed claim. The repository's speed evidence is a capture of a scene on main and on the change, and no scene runs this check on a large graph: the only one known is 0x200c520000 under the forced fallback, and its scene ends before its next present (fix(shader): structurize a 4,750-block graph before Unreal's render thread watchdog fires #9702 explains why).

Same result:

  • A differential fuzzer compared fix(shader): structurize a 4,750-block graph before Unreal's render thread watchdog fires #9702's structurizer with this one on generated graphs (nested selections and loops with breaks, continues, early returns and cross edges; DAGs with back edges; small random graphs with irreducible parts, unreachable blocks and exitless loops). It ran 40,000 graphs of up to 83 blocks and 10,000 of up to 601 blocks in each of default, APS5_NO_PERF_DOM_INTERSECTION, APS5_NO_PERF_ITERATION_SELECTION, APS5_FORCE_STRUCTURIZER_FALLBACK and APS5_NO_PERF_DOMINATOR_TREE: 250,000 comparisons of GraphToString (or the exception text), no differences.
  • A build that runs the old and the new check side by side compared them on all 256,744 checks those runs made, and on 923,416 altered graphs (structured graphs with edges or constructs changed, of which 68% fail the check). They never disagreed.
  • 0x200c520000's structured graph is byte-identical in all five modes, and the side-by-side check agrees on it.

Tests on the head, 6ba7289 on main 7e2ba73 (on top of #9702), Windows 11, RTX 4090, NVIDIA 616.92, WinLibs GCC 15.2: agc_structurizer, agc_shader_control_flow, agc_driver_compute_table_calls and scalar_call pass 11/11 with the intersection_off, iteration_selection_off, dominator_tree_off and forced_fallback variants, each run three times (ctest --repeat until-fail:3).

Validation: I ran the same focused tests on this head with VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation (Vulkan SDK 1.4.363.0), ANYPS5_NO_SHADER_CACHE=1 and ANYPS5_REQUIRE_VULKAN=1, using ctest -R "^(agc_structurizer|agc_shader_control_flow|scalar_call|agc_driver_compute_table_calls)" -V. They pass 11/11. The log has no was not found but was requested by env var VK_INSTANCE_LAYERS, no VUID- line and no validation message.

After the main merge, Windows 11, RTX 4090, NVIDIA 616.92, WinLibs GCC 15.2: 958f3f9 merges main 7b32a84, through #9702's merge. agc_structurizer, agc_shader_control_flow, agc_driver_compute_table_calls and scalar_call pass 11/11 with the intersection_off, iteration_selection_off, dominator_tree_off and forced_fallback variants, each run three times (ctest --repeat until-fail:3). Under VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation (Vulkan SDK 1.4.363.0), ANYPS5_NO_SHADER_CACHE=1 and ANYPS5_REQUIRE_VULKAN=1, with ctest -V, they pass 11/11; on main 4d57821, five commits past the merged main and none in core/shader, the nine of them that exist there pass 9/9 (the two dominator_tree_off variants come with #9702). Neither log has was not found but was requested by env var VK_INSTANCE_LAYERS, a VUID- line or any other validation message.

Checklist

  • Based on current main; no other open PR implements the same functions. No open PR changes Structurizer.cpp.
  • 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: fix(shader): structurize a 4,750-block graph before Unreal's render thread watchdog fires #9702
  • Fixes: none
  • Unblocks: none
  • GPUs tested: NVIDIA GeForce RTX 4090, driver 616.92, Windows 11
  • AI-assisted: yes (Claude Code)

…f scanning them

Merge splitting re-examines every selection header after each split, so the lookups behind each examination run millions of times on a large graph. On Ghostrunner's compute shader 0x200c520000 (4,750 blocks growing to 10,170 over 1,302 splits) agc_shader_replay spent 173 s in splitSharedMergeBlocks. A profile put most of it in linear membership tests and in FindNearestCommonPostDominator, not in the analyses.

Loop bodies, construct blocks, selection regions and strongly connected components are sorted, so membership in them is now a binary search. The traversals that tested membership in the list they were building (selectionRegion, reachableWithin, naturalLoopBody) and externalEntryEdges test a bitset instead, and selectionRegion reads its sorted result from the bitset. dominatedBlocks tests the header once and pushes a successor only before it is visited. FindNearestCommonPostDominator skips a candidate whose post-dominator set is smaller than the other common post-dominators and checks the rest in one pass over the set. remapIds sorts only when the map broke the order.

Each change keeps the same sets in the same order and the same first candidate, so the structured graph is unchanged. On an M4 Pro the shader's structurization takes 37 s instead of 176 s.
…rating dominator sets

recomputeAnalyses runs after every merge split, and its dominator and post-dominator fixpoints intersected a bitset of every block for every block until nothing changed. They now build the tree with the Cooper-Harvey-Kennedy iteration over a reverse post-order from a virtual root above the entry block, or above every exit for post-dominators, and write each block's sorted set as its parent's set plus the block itself.

The sets are the same. When every block is reachable from the virtual root, the greatest fixpoint of the old equations is the dominance relation of that rooted graph, which the tree describes. When a block is unreachable from the entry, or cannot reach an exit, the old fixpoint runs as before.

The post-dominator tree also keeps each block's immediate post-dominator, and FindNearestCommonPostDominator climbs it when both blocks have one: the sets are the tree's paths, so the deepest common block is the one the set intersection found. Blocks added or cloned after the analysis have no parent and take the set path, as before.

APS5_NO_PERF_DOMINATOR_TREE turns both off, and the structurizer and control-flow tests also run with it set. On an M4 Pro the structurization of Ghostrunner's compute shader 0x200c520000 takes 18 s instead of 37 s.
computeStronglyConnectedComponents visited blocks recursively, one stack frame per block on the deepest path. The reach-flag fallback turns Ghostrunner's compute shader 0x200c520000 into a graph of 32,490 blocks, and with APS5_FORCE_STRUCTURIZER_FALLBACK the visit overflowed agc_shader_replay's 2 MiB stack after 4 s.

The visit now keeps its frames in a vector. It enters successors in the same order and closes components in the same order, so the components, their entry blocks and the irreducibility verdict are unchanged.
After each split, every selection header that needs no work still walks its construct (dominatedBlocks) to learn which predecessors of its merge lie inside. On Ghostrunner's compute shader 0x200c520000 that is 1.7 million walks.

When every block is reachable from the entry and the predecessor lists match the successor lists, and the merge M neither is the header H nor dominates it, the walk finds exactly the blocks that H dominates and M does not, other than M. A block the walk reaches is dominated by H and reached from the entry on a path that avoids M, because some path to H avoids M. A block that H dominates and M does not lies on a path from the entry that avoids M, and from the last H on that path every block is dominated by H, so the walk reaches it. splitOneSelectionMerge checks those conditions once per pass and then asks the dominator sets directly; otherwise it walks as before. Only membership in the construct is used, so the split and its order are unchanged.

On an M4 Pro the shader's structurization takes 12 to 15 s instead of 18 s, and 176 s before this series.
…ucts

hasStructuredConstructs tested every block of the graph against each merge header, and searched every block for a block's innermost loop, with dominator binary searches each time. Under APS5_FORCE_STRUCTURIZER_FALLBACK, Ghostrunner's compute shader 0x200c520000 becomes a 32,490-block reach-flag graph with 6,647 merge headers, and the check took 44 to 50 s of the 49 to 55 s.

The innermost loop is now searched among the loop headers only. Every other block failed the loop-header test first, so the candidates and their order are unchanged.

When constructsFollowDominance holds for the structural graph, the member checks visit only the blocks dominatedBlocks finds from the header, stopping at its merge, and for a loop also from its continue block, in block order. Only a block in the header's construct or in the loop's continue construct can fail a member check. A construct block is dominated by the header and not by the merge, so some path from the entry reaches it without the merge, and from the last header on that path every block is dominated by the header, so the walk reaches it. A continue-construct block is dominated by the continue block and is reached from it the same way. The walk finds every such block, and the blocks it skips passed every check, so the result is unchanged. Otherwise every block is checked as before, and APS5_NO_PERF_DOMINATOR_TREE keeps the old loop.

On an M4 Pro the check takes 0.27 s instead of 44 to 50 s on that graph, and the forced fallback takes 5.1 to 5.4 s instead of 49 to 55 s. The default structurization of the shader makes one check, which takes 0.03 s instead of 0.36 s.
@github-actions github-actions Bot added perf Performance change perf numbers missing perf pull request without a before and after summary line labels Oct 22, 2026
@github-actions

Copy link
Copy Markdown

This pull request is labeled perf, so it needs the summary line of a capture on main and one with the change, from the same title, scene and machine, as the Performance report form asks. Please paste both aps5-summary.jsonl lines, attach the two traces and add the output of tools/trace_digest.py.

@clairtonmichel

Copy link
Copy Markdown

Tested on AMD Ryzen 7 9800X3D and AMD Radeon RX 7900 XTX (Fedora Linux, Mesa 26.2.5):

  • Built cleanly with zero errors.
  • Ran the shader test suite (63/63 tests): 100% passed successfully in 0.09s / 9.89s total.
  • Shader construct traversal optimization is stable. Ready to merge!

@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 1896 docs/dev/ENVIRONMENT.md
PR 6924 docs/dev/ENVIRONMENT.md
PR 7449 docs/dev/ENVIRONMENT.md
PR 7457 docs/dev/ENVIRONMENT.md
PR 7484 docs/dev/ENVIRONMENT.md
PR 7604 docs/dev/ENVIRONMENT.md
PR 7642 docs/dev/ENVIRONMENT.md
PR 8004 docs/dev/ENVIRONMENT.md
PR 8246 docs/dev/ENVIRONMENT.md
PR 8484 docs/dev/ENVIRONMENT.md
PR 8542 docs/dev/ENVIRONMENT.md
PR 9176 docs/dev/ENVIRONMENT.md
PR 9248 docs/dev/ENVIRONMENT.md
PR 9287 docs/dev/ENVIRONMENT.md
PR 9289 docs/dev/ENVIRONMENT.md
PR 9345 docs/dev/ENVIRONMENT.md
PR 9356 docs/dev/ENVIRONMENT.md
PR 9387 docs/dev/ENVIRONMENT.md
PR 9508 docs/dev/ENVIRONMENT.md
PR 9676 docs/dev/ENVIRONMENT.md
PR 9852 docs/dev/ENVIRONMENT.md
PR 10193 docs/dev/ENVIRONMENT.md
PR 10442 docs/dev/ENVIRONMENT.md
PR 10547 docs/dev/ENVIRONMENT.md
PR 11207 docs/dev/ENVIRONMENT.md

@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.

I reviewed the construct-walk optimization against the previous hasStructuredConstructs logic, including the structural graph analysis, the loop/continue member sets, and the fallback path. The optimized walk is guarded by constructsFollowDominance, which checks reachability and predecessor/successor consistency; the original full scan remains as fallback. The reported differential comparisons and repeated focused/Vulkan-validation runs are meaningful evidence. I found no independent code defect in the reviewed diff. I could not run the C++ suite locally because this checkout has no configured product build.

Before approval, please satisfy the repository performance-report rule: this PR is labeled perf, but the description does not include matching aps5-summary.jsonl lines for main and the change from the same title, scene, and machine, the two trace attachments, or tools/trace_digest.py output. The workflow has marked perf numbers missing. Please add the capture artifacts, or remove the performance claim/title if the required capture is unavailable.

This branch is based on 7e2ba73e4c, 13 commits behind current main (8b3a6ba88d8fc74e768f1a7e8e9e36fb69842768), and depends on #9702, which has the same base age and needs the same performance-report artifacts. Please keep the declared dependency, coordinate the stack after #9702 is updated, and rebase/retest this head on current main before requesting another review.

Local checks I ran against the head: git diff --check passed; tools/check_conventions.py passed with the actual PR body. GitHub conventions, eligibility, intake, labels, and report checks pass; product build jobs are skipped by the repository gates. I did not independently reproduce the reported timings, differential fuzzing, or platform/GPU test results.

@frankea frankea changed the title perf(shader): walk each construct when checking the structured constructs refactor(shader): walk each construct when checking the structured constructs Oct 23, 2026
@github-actions github-actions Bot removed perf Performance change perf numbers missing perf pull request without a before and after summary line labels Oct 23, 2026
@frankea

frankea commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

@MarcZX10 thank you for the review. The stack now merges main 7b32a84. This head is 958f3f9, through #9702's merge. On the RTX 4090, the focused tests pass 11/11 three times and under the validation layer, with no validation message, and main 4d57821 passes its nine the same way.

No scene can give the capture: the only graph where this check is large is Ghostrunner's 0x200c520000 under the forced fallback, and that scene ends before its next present. So I've taken the other route you offered. The title is now refactor(shader): ..., and the body makes no speed claim. It keeps the evidence that the result is unchanged: the differential fuzzer, the side-by-side check over 256,744 checks and 923,416 altered graphs, and the byte-identical structured graph of that shader.

AI-assisted: yes (Claude Code)

@frankea

frankea commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

@MarcZX10 since your review this PR is retitled as a refactor with no speed claim (the perf labels came off), and it now merges #9702's refreshed head on main 0db47b2 (head 98c6aef); the tests and the paired validation-layer comparison are rerun on it (first bullet under Tested).

AI-assisted: yes (Claude Code)

@adriadam10 adriadam10 self-assigned this Oct 24, 2026
@adriadam10
adriadam10 dismissed MarcZX10’s stale review October 26, 2026 11:17

Dismissing a stale review: the requested changes were addressed after it (new head) and the merge queue blocks on it. Please request changes again if anything is still open.

@adriadam10
adriadam10 merged commit 7992f03 into boykopovar:main Oct 26, 2026
12 checks passed
@adriadam10

Copy link
Copy Markdown
Collaborator

Checked the construct walk in hasStructuredConstructs against the old loop: the loop-header-only search keeps the same candidates and order, inRegion is the same predicate as the old inline one, and the walk from the header (and the continue block) covers every block that can fail a member check; the full scan stays as the fallback and under APS5_NO_PERF_DOMINATOR_TREE. The differential runs and the title report on Ghostrunner agree. The one red Windows test (check_compile_modes_test_library_directory_values_can_look_like_modes, a 30 s timeout of a tools script) is unrelated to this diff and was rerun; merged.

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.

4 participants