shader cfg: structurize overlapping early-exit ladders

Trace the dispatcher fallback for PS 0x9e1133a6 to an acyclic RDNA2
S_CBRANCH_SCC0 ladder. Several inner headers reach both a shared
continuation and a shared terminal, so they do not dominate the common
post-dominator. The old shared-merge splitter redirected only the dominated
terminal edge; the merge therefore remained shared and forced dispatcher
lowering.

Canonicalize this control flow with bounded cross-entry tail duplication:
- find the complete selection region before its post-dominator
- clone only region blocks that the header does not dominate
- redirect header-owned edges into those clones
- join every owned exit through a private synthetic merge
- retain the existing dispatcher when loops, invalid merges, or growth bounds
  make duplication inappropriate

Add the recovered seven-block topology as a focused regression. Assert SCC0
taken/fallthrough orientation, unique merges, full post-transform reachability,
structured lowering without OpSwitch, and valid SPIR-V.

Validation:
- shader_cfg_tests --overlapping-cfg-only
- shader_cfg_tests --loop-break-merge-only
- shader_cfg_tests --loop-canonicalization-only
- kyty_emulator built with _Build/vscode-clang
- launch.json visible run advanced continuously to frame 570
- observed seven-block PS e83e3fb5 structured into 19 blocks without fallback
- run stopped at the separately deferred sparse PRT BufferCache backing fatal

Independent audits verified progress, ID remapping, vector lifetime, invalid
merge handling, bounded termination, and dirty-tree staging scope.
This commit is contained in:
nmzik
2026-08-02 03:16:08 +02:00
parent 7a7b39e9b9
commit cb6ead58dc
2 changed files with 193 additions and 4 deletions
@@ -1098,7 +1098,102 @@ bool SplitOneLoopMerge(Graph& graph) {
return false;
}
bool SplitOneSelectionMerge(Graph& graph) {
std::vector<uint32_t> SelectionRegion(const Graph& graph, const BasicBlock& header,
uint32_t merge) {
std::vector<uint32_t> region;
std::vector<uint32_t> pending = {header.terminator.true_block,
header.terminator.false_block};
while (!pending.empty()) {
const auto block_id = pending.back();
pending.pop_back();
if (block_id == merge || Contains(region, block_id)) {
continue;
}
const auto* block = graph.FindBlock(block_id);
if (block == nullptr) {
continue;
}
AddUnique(region, block_id);
pending.insert(pending.end(), block->successors.begin(), block->successors.end());
}
SortUnique(region);
return region;
}
bool DuplicateSelectionRegion(Graph& graph, uint32_t header_id, uint32_t merge,
const std::vector<uint32_t>& region, uint32_t block_budget) {
std::vector<uint32_t> cloned_blocks;
for (auto block_id: region) {
if (!graph.Dominates(header_id, block_id)) {
cloned_blocks.push_back(block_id);
}
}
if (cloned_blocks.empty() || graph.FindBlock(header_id) == nullptr || header_id >= merge ||
graph.blocks.size() + cloned_blocks.size() + 1u > block_budget) {
return false;
}
const auto first_clone = static_cast<uint32_t>(graph.blocks.size());
std::map<uint32_t, uint32_t> clones;
for (uint32_t i = 0; i < cloned_blocks.size(); i++) {
clones.emplace(cloned_blocks[i], first_clone + i);
}
for (auto block_id: cloned_blocks) {
BasicBlock clone = *graph.FindBlock(block_id);
clone.id = clones.at(block_id);
clone.predecessors.clear();
clone.dominators.clear();
clone.post_dominators.clear();
graph.blocks.push_back(std::move(clone));
}
const auto remap_block = [&](BasicBlock& block) {
const auto remap_target = [&](uint32_t& target) {
if (const auto it = clones.find(target); it != clones.end()) {
target = it->second;
}
};
for (auto& successor: block.successors) {
remap_target(successor);
}
remap_target(block.terminator.true_block);
remap_target(block.terminator.false_block);
remap_target(block.terminator.merge_block);
remap_target(block.terminator.continue_block);
for (auto& target: block.terminator.indirect_targets) {
remap_target(target);
}
};
for (auto block_id: region) {
const auto owned_id = clones.contains(block_id) ? clones.at(block_id) : block_id;
remap_block(*graph.FindBlock(owned_id));
}
const auto private_merge = AppendSyntheticBranchBlock(graph, merge);
auto& header = *graph.FindBlock(header_id);
remap_block(header);
for (auto block_id: region) {
const auto owned_id = clones.contains(block_id) ? clones.at(block_id) : block_id;
auto* block = graph.FindBlock(owned_id);
if (block != nullptr) {
ReplaceValue(block->successors, merge, private_merge);
ReplaceTerminatorTarget(block->terminator, merge, private_merge);
}
}
ReplaceValue(header.successors, merge, private_merge);
ReplaceTerminatorTarget(header.terminator, merge, private_merge);
for (uint32_t i = 0; i <= cloned_blocks.size(); i++) {
MoveBlockBefore(graph, first_clone + i, merge + i);
}
RebuildPredecessors(graph);
RecomputeAnalyses(graph);
return true;
}
bool SplitOneSelectionMerge(Graph& graph, uint32_t block_budget) {
std::vector<uint32_t> loop_headers;
loop_headers.reserve(graph.natural_loops.size());
for (const auto& loop: graph.natural_loops) {
@@ -1118,6 +1213,18 @@ bool SplitOneSelectionMerge(Graph& graph) {
const auto merge = graph.FindNearestCommonPostDominator(block->terminator.true_block,
block->terminator.false_block);
if (merge == UINT32_MAX || graph.FindBlock(merge) == nullptr) {
continue;
}
const auto region = SelectionRegion(graph, *block, merge);
if (std::any_of(region.begin(), region.end(),
[&](uint32_t member) { return !graph.Dominates(block_id, member); })) {
if (graph.natural_loops.empty() &&
DuplicateSelectionRegion(graph, block_id, merge, region, block_budget)) {
return true;
}
continue;
}
const auto construct_blocks = DominatedBlocks(graph, block_id, merge);
const auto force_split = MergeLeavesContainingLoop(graph, block_id, merge);
if (SplitSharedMergeBlock(graph, merge, construct_blocks, force_split)) {
@@ -1129,10 +1236,12 @@ bool SplitOneSelectionMerge(Graph& graph) {
bool SplitSharedMergeBlocks(Graph& graph, std::string* error) {
const auto original_block_count = static_cast<uint32_t>(graph.blocks.size());
const auto split_budget =
std::max<uint32_t>(16u, std::min<uint32_t>(128u, original_block_count));
const auto split_budget = std::max<uint32_t>(
16u, std::min<uint32_t>(128u, original_block_count * 4u));
const auto block_budget = std::max<uint32_t>(
32u, std::min<uint32_t>(512u, original_block_count * 8u));
for (uint32_t splits = 0; splits < split_budget; splits++) {
if (!SplitOneLoopMerge(graph) && !SplitOneSelectionMerge(graph)) {
if (!SplitOneLoopMerge(graph) && !SplitOneSelectionMerge(graph, block_budget)) {
return true;
}
RebuildPredecessors(graph);
+80
View File
@@ -5770,6 +5770,85 @@ void TestNewShaderRecompilerCfgDuplicateMergeStructuredSplit() {
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerCfgOverlappingEarlyExitLadder() {
const uint32_t shader[] = {
EncodeSopc(0x06, 0, 0), // block 0
EncodeSopp(0x04, 2), // block 0 -> 2 or 1
EncodeSopc(0x06, 1, 1), // block 1
EncodeSopp(0x04, 6), // block 1 -> 5 or 2
EncodeSopc(0x06, 2, 2), // block 2
EncodeSopp(0x04, 4), // block 2 -> 5 or 3
EncodeSopc(0x06, 3, 3), // block 3
EncodeSopp(0x04, 2), // block 3 -> 5 or 4
EncodeSMovB32(4, 129), // block 4
0xbf810000u, // block 4 -> 6
EncodeSMovB32(5, 129), // block 5
0xbf810000u, // block 5 -> 6
};
ShaderRecompiler::Decoder::Program decoded;
std::string error;
Check(ShaderRecompiler::Decoder::DecodeProgram(std::span {shader}, decoded, &error),
error.c_str());
ShaderRecompiler::CFG::Graph graph;
Check(ShaderRecompiler::CFG::BuildGraph(decoded, graph, &error), error.c_str());
Check(graph.blocks.size() == 7u && graph.blocks[0].successors == std::vector<uint32_t>({1, 2}) &&
graph.blocks[0].terminator.true_block == 2u &&
graph.blocks[0].terminator.false_block == 1u &&
graph.blocks[1].successors == std::vector<uint32_t>({2, 5}) &&
graph.blocks[1].terminator.true_block == 5u &&
graph.blocks[1].terminator.false_block == 2u &&
graph.blocks[2].successors == std::vector<uint32_t>({3, 5}) &&
graph.blocks[2].terminator.true_block == 5u &&
graph.blocks[2].terminator.false_block == 3u &&
graph.blocks[3].successors == std::vector<uint32_t>({4, 5}) &&
graph.blocks[3].terminator.true_block == 5u &&
graph.blocks[3].terminator.false_block == 4u &&
graph.blocks[4].successors == std::vector<uint32_t>({6}) &&
graph.blocks[5].successors == std::vector<uint32_t>({6}),
"overlapping early-exit fixture does not match the observed shader CFG");
Check(ShaderRecompiler::CFG::Structurize(graph, &error), error.c_str());
std::vector<bool> reachable(graph.blocks.size());
std::vector<uint32_t> pending = {graph.entry_block};
while (!pending.empty()) {
const auto block_id = pending.back();
pending.pop_back();
if (reachable[block_id]) {
continue;
}
reachable[block_id] = true;
pending.insert(pending.end(), graph.blocks[block_id].successors.begin(),
graph.blocks[block_id].successors.end());
}
Check(std::all_of(reachable.begin(), reachable.end(), [](bool value) { return value; }),
"overlapping early-exit structurization left unreachable blocks");
std::vector<uint32_t> merges;
for (const auto& block: graph.blocks) {
if (block.terminator.kind == ShaderRecompiler::CFG::TerminatorKind::ConditionalBranch) {
Check(block.terminator.merge_block != UINT32_MAX &&
std::find(merges.begin(), merges.end(), block.terminator.merge_block) ==
merges.end(),
"overlapping early-exit structurization retained a shared merge");
merges.push_back(block.terminator.merge_block);
}
}
ShaderRecompiler::CompileOptions options;
options.stage = ShaderType::Pixel;
options.dump_ir = true;
ShaderRecompiler::CompileResult result;
Check(ShaderRecompiler::TryRecompile(shader, options, result, &error), error.c_str());
Check(Common::ContainsStr(result.ir_dump, "mode=structured"),
"overlapping early-exit ladder did not stay on the structured path");
Check(!Common::ContainsStr(result.ir_dump, "duplicate structured merge block"),
"overlapping early-exit ladder retained a shared merge");
Check(SpirvInstructionOpcodeCount(result.spirv, 247) >= 4u,
"overlapping early-exit ladder lost its selections");
Check(SpirvInstructionOpcodeCount(result.spirv, 251) == 0u,
"overlapping early-exit ladder used dispatcher OpSwitch");
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerCfgIrreducibleDispatcher() {
const uint32_t shader[] = {
EncodeSopp(0x05, 2), // entry -> B, fallthrough A
@@ -7409,6 +7488,7 @@ int main() {
TestNewShaderRecompilerCfgConditionalLoopHeaderSelection();
TestNewShaderRecompilerCfgMultipleLoopLatches();
TestNewShaderRecompilerCfgDuplicateMergeStructuredSplit();
TestNewShaderRecompilerCfgOverlappingEarlyExitLadder();
TestNewShaderRecompilerCfgIrreducibleDispatcher();
TestNewShaderRecompilerExecMaskHelpers();
TestComputeShaderInputWaveSize();