From 44d7f2a3e88d2c45a46a4a03aefca0393789c150 Mon Sep 17 00:00:00 2001 From: nmzik Date: Sat, 1 Aug 2026 23:40:11 +0200 Subject: [PATCH] shader cfg: handle shared early exits Duplicate small shared exit tails so each selection gets its own merge block. This keeps overlapping early-exit ladders on structured SPIR-V and adds a regression test. --- .../shader/recompiler/cfg/ShaderCFG.cpp | 117 +++++++++++++++++- tests/shaderCfgTests.cpp | 80 ++++++++++++ 2 files changed, 193 insertions(+), 4 deletions(-) diff --git a/src/graphics/shader/recompiler/cfg/ShaderCFG.cpp b/src/graphics/shader/recompiler/cfg/ShaderCFG.cpp index b46101d..58df77f 100644 --- a/src/graphics/shader/recompiler/cfg/ShaderCFG.cpp +++ b/src/graphics/shader/recompiler/cfg/ShaderCFG.cpp @@ -1098,7 +1098,102 @@ bool SplitOneLoopMerge(Graph& graph) { return false; } -bool SplitOneSelectionMerge(Graph& graph) { +std::vector SelectionRegion(const Graph& graph, const BasicBlock& header, + uint32_t merge) { + std::vector region; + std::vector 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& region, uint32_t block_budget) { + std::vector 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(graph.blocks.size()); + std::map 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 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(graph.blocks.size()); - const auto split_budget = - std::max(16u, std::min(128u, original_block_count)); + const auto split_budget = std::max( + 16u, std::min(128u, original_block_count * 4u)); + const auto block_budget = std::max( + 32u, std::min(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); diff --git a/tests/shaderCfgTests.cpp b/tests/shaderCfgTests.cpp index 4f0cfbb..4be35b2 100644 --- a/tests/shaderCfgTests.cpp +++ b/tests/shaderCfgTests.cpp @@ -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({1, 2}) && + graph.blocks[0].terminator.true_block == 2u && + graph.blocks[0].terminator.false_block == 1u && + graph.blocks[1].successors == std::vector({2, 5}) && + graph.blocks[1].terminator.true_block == 5u && + graph.blocks[1].terminator.false_block == 2u && + graph.blocks[2].successors == std::vector({3, 5}) && + graph.blocks[2].terminator.true_block == 5u && + graph.blocks[2].terminator.false_block == 3u && + graph.blocks[3].successors == std::vector({4, 5}) && + graph.blocks[3].terminator.true_block == 5u && + graph.blocks[3].terminator.false_block == 4u && + graph.blocks[4].successors == std::vector({6}) && + graph.blocks[5].successors == std::vector({6}), + "overlapping early-exit fixture does not match the observed shader CFG"); + Check(ShaderRecompiler::CFG::Structurize(graph, &error), error.c_str()); + std::vector reachable(graph.blocks.size()); + std::vector 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 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();