From 99a87bd65be8eec0e874be24d6902302bcf06ca6 Mon Sep 17 00:00:00 2001 From: nmzik Date: Sun, 6 Sep 2026 10:23:44 +0400 Subject: [PATCH] shader: keep loop branches during selection routing --- .../recompiler/frontend/cfg/ShaderCFG.cpp | 12 +++- tests/shaderCfgTests.cpp | 55 +++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/graphics/shader/recompiler/frontend/cfg/ShaderCFG.cpp b/src/graphics/shader/recompiler/frontend/cfg/ShaderCFG.cpp index 7a708d91..e5606745 100644 --- a/src/graphics/shader/recompiler/frontend/cfg/ShaderCFG.cpp +++ b/src/graphics/shader/recompiler/frontend/cfg/ShaderCFG.cpp @@ -1718,7 +1718,8 @@ void AppendEnclosingRouteBlocks(Graph& graph, uint32_t original_block_count, for (uint32_t candidate = 0; candidate < original_block_count; candidate++) { const auto* candidate_block = graph.FindBlock(candidate); if (candidate_block == nullptr || - candidate_block->terminator.kind != TerminatorKind::ConditionalBranch) { + candidate_block->terminator.kind != TerminatorKind::ConditionalBranch || + IsInnermostLoopControlConditional(graph, *candidate_block)) { continue; } const auto candidate_true = candidate_block->terminator.true_block; @@ -1752,11 +1753,15 @@ void AppendEnclosingRouteBlocks(Graph& graph, uint32_t original_block_count, // Turn H0 -> shared/H1, H1 -> shared/other into nested selections whose empty // forwarding blocks set one typed SSA route value. Guest semantic blocks remain unique. +// Preserve loop-control branches just as SplitOneSelectionMerge does. Moving a loop +// exit through the route selector can create an inner cycle whose merge is also the +// enclosing loop merge, so splitting that shared exit can never separate the loops. bool RouteOneSharedArm(Graph& graph, uint32_t original_block_count, uint32_t outer_id, uint32_t route_variable, GotoRouteBlocks& route) { auto* outer = graph.FindBlock(outer_id); if (outer == nullptr || outer->inst_begin == outer->inst_end || - outer->terminator.kind != TerminatorKind::ConditionalBranch) { + outer->terminator.kind != TerminatorKind::ConditionalBranch || + IsInnermostLoopControlConditional(graph, *outer)) { return false; } @@ -1768,7 +1773,8 @@ bool RouteOneSharedArm(Graph& graph, uint32_t original_block_count, uint32_t out const auto* inner = graph.FindBlock(inner_id); if (inner == nullptr || inner->inst_begin == inner->inst_end || inner->predecessors != std::vector {outer_id} || - inner->terminator.kind != TerminatorKind::ConditionalBranch) { + inner->terminator.kind != TerminatorKind::ConditionalBranch || + IsInnermostLoopControlConditional(graph, *inner)) { continue; } diff --git a/tests/shaderCfgTests.cpp b/tests/shaderCfgTests.cpp index 77184786..524ad095 100644 --- a/tests/shaderCfgTests.cpp +++ b/tests/shaderCfgTests.cpp @@ -8375,6 +8375,60 @@ void TestNewShaderRecompilerCfgLoopSharedRegion() { CheckSpirvBinaryValidates(result.spirv); } +void TestNewShaderRecompilerCfgSharedRegionBeforeEarlyBreakLoop() { + // The first selection needs routing at its externally entered common tail. + // A later loop has the same lexical shared-arm shape, but its exit branches + // are already structured and must retain their loop-control role. + const uint32_t shader[] = { + EncodeSopc(0x06, 0, 0), // 0: enclosing selection + EncodeSopp(0x04, 4), // 0 -> 3 or 1 + EncodeSopc(0x06, 1, 1), // 1: inner selection + EncodeSopp(0x04, 6), // 1 -> 5 or 2 + EncodeSMovB32(2, 129), // 2: inner body + EncodeSopp(0x02, 2), // 2 -> 4 + EncodeSMovB32(3, 129), // 3: outer arm + EncodeSopp(0x02, 0), // 3 -> 4 + EncodeSMovB32(4, 129), // 4: shared tail + EncodeSopp(0x02, 0), // 4 -> 5 + EncodeSopc(0x06, 5, 5), // 5: loop header + EncodeSopp(0x04, 4), // 5 -> 8 or 6 + EncodeSopc(0x06, 6, 6), // 6: early break + EncodeSopp(0x04, 2), // 6 -> 8 or 7 + EncodeSopc(0x06, 7, 7), // 7: loop latch + EncodeSopp(0x05, 0xfffau), // 7 -> 5 or 8 + EncodeSMovB32(8, 129), // 8: terminal + 0xbf810000u, + }; + + ShaderRecompiler::Decoder::Program decoded; + ShaderRecompiler::Decoder::DecodeProgram(std::span{shader}, decoded); + auto graph = ShaderRecompiler::CFG::BuildGraph(decoded); + const auto original_coverage = + CfgInstructionCoverage(graph, decoded.instructions.size()); + Check(graph.blocks.size() == 9u && graph.natural_loops.size() == 1u, + "shared-region/early-break fixture has the wrong native CFG"); + Check(ShaderRecompiler::CFG::Structurize(graph), + graph.unsupported_reason.c_str()); + Check(graph.natural_loops.size() == 1u && graph.back_edges.size() == 1u, + "selection routing introduced a cycle around a structured loop exit"); + Check(std::ranges::count_if(graph.blocks, [](const auto &block) { + return block.terminator.condition == + ShaderRecompiler::CFG::BranchCondition::GotoVariable; + }) == 1u, + "shared-region routing rewrote unrelated loop-control branches"); + Check(CfgInstructionCoverage(graph, decoded.instructions.size()) == + original_coverage, + "shared-region/early-break routing changed semantic coverage"); + + auto options = MakeCompileOptions(ShaderType::Compute); + auto result = RecompileForTest(shader, options); + Check(!result.program.dispatcher_fallback && + SpirvInstructionOpcodeCount(result.spirv, 246) == 1u && + SpirvInstructionOpcodeCount(result.spirv, 251) == 0u, + "shared-region/early-break routing lost structured loop control"); + CheckSpirvBinaryValidates(result.spirv); +} + void TestNewShaderRecompilerCfgOverlappingEarlyExitLadder() { const uint32_t shader[] = { EncodeSopc(0x06, 0, 0), // block 0 @@ -12676,6 +12730,7 @@ int main() { TestNewShaderRecompilerCfgNestedTailEarlyExit(); TestNewShaderRecompilerCfgSharedReturnAfterNestedSelections(); TestNewShaderRecompilerCfgLoopSharedRegion(); + TestNewShaderRecompilerCfgSharedRegionBeforeEarlyBreakLoop(); TestNewShaderRecompilerCfgOverlappingEarlyExitLadder(); TestNewShaderRecompilerCfgNestedEarlyExitSharedTerminal(); TestNewShaderRecompilerCfgSharedTerminalEarlyExit();