From c9ec82e32e59c36912407ffb17c1cc00753e45ea Mon Sep 17 00:00:00 2001 From: nmzik Date: Tue, 13 Oct 2026 01:03:46 +0200 Subject: [PATCH] shader: factor live execution guards before resource planning --- .../shader/recompiler/ShaderRecompiler.cpp | 17 +++++----- .../ir/passes/ConstantPropagation.cpp | 28 ++++++++++++++++- tests/ResourceTrackingTests.cpp | 31 ++++++++++++++----- tests/ShaderRecompilerComputeTests.cpp | 6 +++- 4 files changed, 65 insertions(+), 17 deletions(-) diff --git a/src/graphics/shader/recompiler/ShaderRecompiler.cpp b/src/graphics/shader/recompiler/ShaderRecompiler.cpp index be96be3e5..3612aa9ae 100644 --- a/src/graphics/shader/recompiler/ShaderRecompiler.cpp +++ b/src/graphics/shader/recompiler/ShaderRecompiler.cpp @@ -789,19 +789,20 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO GetDumpLabel(options), StageName(options.stage), options.shader_hash, static_cast(ir.blocks.size()), phase_ms()); IR::RewriteToSsa(ir.blocks); - IR::ConstantPropagationPass(ir.blocks, ir.wave_size); - IR::ResolveControlFlowIdentities(ir); - IR::RemoveIdentities(ir.blocks); - IR::EliminateDeadCode(ir.blocks); - const auto read_lane_stats = IR::EliminateReadLane(ir, ir.wave_size); - if (read_lane_stats.rewritten_reads != 0) { - LOGF("%s read-lane elimination: reads=%" PRIu32 "\n", GetDumpLabel(options), - read_lane_stats.rewritten_reads); + const auto simplify = [&ir] { IR::ConstantPropagationPass(ir.blocks, ir.wave_size); IR::ResolveControlFlowIdentities(ir); IR::RemoveIdentities(ir.blocks); IR::EliminateDeadCode(ir.blocks); + }; + simplify(); + const auto read_lane_stats = IR::EliminateReadLane(ir, ir.wave_size); + if (read_lane_stats.rewritten_reads != 0) { + LOGF("%s read-lane elimination: reads=%" PRIu32 "\n", GetDumpLabel(options), + read_lane_stats.rewritten_reads); } + // Cleanup also exposes profitable folds by removing dead predicate consumers. + simplify(); if (decoded.has_swap_pc) { // Leaf returns are resolved CFG edges; native PC values must not escape into GPU data. for (const auto* block: ir.blocks) diff --git a/src/graphics/shader/recompiler/ir/passes/ConstantPropagation.cpp b/src/graphics/shader/recompiler/ir/passes/ConstantPropagation.cpp index 1a4acd017..543951fbd 100644 --- a/src/graphics/shader/recompiler/ir/passes/ConstantPropagation.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ConstantPropagation.cpp @@ -22,6 +22,13 @@ void Replace(Inst& inst, Value value) { inst.ReplaceUsesWith(value.Resolve()); } +bool HasOneUse(const Inst& inst) { + // Replacement aliases preserve external Values but emit no additional operation. + return std::ranges::count_if(inst.Uses(), [](const Use& use) { + return use.user->GetOpcode() != ValueOpcode::Identity; + }) == 1; +} + class FoldWorklist { public: struct Location { @@ -744,7 +751,7 @@ void FoldInstruction(Block& block, Block::iterator instruction, if (inverse) { test = Arg(*test, 0).TryInstruction(); if (test == nullptr) return false; - if (test->GetOpcode() == ValueOpcode::LogicalAnd) + if (test->GetOpcode() == ValueOpcode::LogicalAnd && HasOneUse(*test)) for (uint32_t arg = 0; arg < 2u; ++arg) if (Arg(*test, arg) == assumption) return replace(negate(Arg(*test, arg ^ 1u))); @@ -786,6 +793,25 @@ void FoldInstruction(Block& block, Block::iterator instruction, Replace(inst, lhs.U1() ? lhs : rhs); } else if (IsImmediate(rhs, Type::U1)) { Replace(inst, rhs.U1() ? rhs : lhs); + } else { + const auto* left = lhs.TryInstruction(); + const auto* right = rhs.TryInstruction(); + if (left == nullptr || right == nullptr || + left->GetOpcode() != ValueOpcode::LogicalAnd || + right->GetOpcode() != ValueOpcode::LogicalAnd || + (!HasOneUse(*left) && !HasOneUse(*right))) return; + // Factor a shared execution predicate without retaining more operations. + for (uint32_t a = 0; a < 2u; ++a) { + for (uint32_t b = 0; b < 2u; ++b) { + const auto common = Arg(*left, a); + if (common != Arg(*right, b)) continue; + const auto either = Value(&*worklist.Prepend(block, instruction, + ValueOpcode::LogicalOr, {Arg(*left, a ^ 1u), Arg(*right, b ^ 1u)})); + Replace(inst, Value(&*worklist.Prepend(block, instruction, + ValueOpcode::LogicalAnd, {common, either}))); + return; + } + } } } return; diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index 516c75f0c..cb6c48b74 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -566,10 +566,11 @@ void TestGuardedDirectImageTable() { Bounded, BoundMissing, BoundUnrelated, BoundBypass, BoundAnyLane, BoundDiamond, BoundBranchSpecific, BoundMismatched, BoundExec, BoundExecUnguarded, BoundExecMismatched, BoundExecAnd, BoundExecUnprovenActive, + BoundExecFactored, Finite, FiniteZero, FiniteMissing, FiniteWrong, FiniteSigned, FiniteAdd, FiniteXor, FiniteShared, FiniteSharedBypass, FiniteLate }; - const auto make_plan = [](Guard guard) { + const auto make_plan = [](Guard guard, uint32_t operand_order = 0u) { const bool finite = guard >= Guard::Finite; const bool bounded = guard >= Guard::Bounded && !finite; const bool exec_bound = bounded && guard >= Guard::BoundExec; @@ -650,8 +651,19 @@ void TestGuardedDirectImageTable() { ? ValueOpcode::BitwiseAnd32 : ValueOpcode::BitwiseOr32, {encoded, varying}); const auto retained = fixture.Emit(ValueOpcode::SelectU32, {previous, stopped, Value(0u)}); - const auto vcc = fixture.Emit(ValueOpcode::LogicalAnd, {previous, - fixture.Emit(ValueOpcode::INotEqual32, {retained, Value(0u)})}); + const auto stopped_lane = fixture.Emit(ValueOpcode::INotEqual32, {retained, Value(0u)}); + auto vcc = fixture.Emit(ValueOpcode::LogicalAnd, + operand_order & 1u ? std::initializer_list{stopped_lane, previous} + : std::initializer_list{previous, stopped_lane}); + if (guard == Guard::BoundExecFactored) { + const auto empty = fixture.Emit(ValueOpcode::IEqual32, {key, Value(UINT32_MAX)}); + const auto sentinel = fixture.Emit(ValueOpcode::LogicalAnd, + operand_order & 2u ? std::initializer_list{empty, previous} + : std::initializer_list{previous, empty}); + vcc = fixture.Emit(ValueOpcode::LogicalOr, + operand_order & 4u ? std::initializer_list{sentinel, vcc} + : std::initializer_list{vcc, sentinel}); + } const auto remaining = fixture.Emit(ValueOpcode::LogicalNot, {vcc}); active = guard == Guard::BoundExecUnprovenActive ? remaining : fixture.Emit(ValueOpcode::LogicalAnd, {previous, remaining}); @@ -799,10 +811,12 @@ void TestGuardedDirectImageTable() { fixture.AddMemory(memory, 0x130)); } if (exec_bound) ConstantPropagationPass(fixture.program.blocks); - if (guard == Guard::BoundExec) { + if (guard == Guard::BoundExec || guard == Guard::BoundExecFactored) { std::vector pending{active.ResolveInstruction()}; + uint32_t conjunctions = 0; for (size_t index = 0; index < pending.size(); ++index) { const auto *inst = pending[index]; + conjunctions += inst->GetOpcode() == ValueOpcode::LogicalAnd; Check(inst->GetOpcode() != ValueOpcode::SelectU32 && inst->GetOpcode() != ValueOpcode::BitwiseOr32, "EXEC bound retained its integer Boolean encoding"); @@ -812,6 +826,7 @@ void TestGuardedDirectImageTable() { pending.push_back(operand); } } + Check(conjunctions == 1u, "EXEC bound retained repeated active-lane conjunctions"); } fixture.PlanAndTrack(); const auto source = fixture.program.info.images[0].source; @@ -829,6 +844,7 @@ void TestGuardedDirectImageTable() { auto plan = make_plan(Guard::Nonzero); make_plan(Guard::SccNonZero); make_plan(Guard::Plain); + for (uint32_t order = 0; order < 8u; ++order) make_plan(Guard::BoundExecFactored, order); for (const auto guard : {Guard::Zero, Guard::Unrelated, Guard::Bypass, Guard::ExecZero, Guard::VccZero, Guard::BoundMissing, Guard::BoundUnrelated, Guard::BoundBypass, Guard::BoundAnyLane, @@ -914,13 +930,14 @@ void TestGuardedDirectImageTable() { "batched descriptor read crossed the 48-bit endpoint"); make_plan(Guard::Bounded); - for (const auto guard : {Guard::BoundDiamond, Guard::BoundExec}) { + for (const auto guard : {Guard::BoundDiamond, Guard::BoundExec, Guard::BoundExecFactored}) { auto bounded = make_plan(guard); Check(bounded.capture_specialization_reads && bounded.info.images[1].written, "bounded table with image writes omitted specialization read capture"); LinearTestMemory records; - const uint32_t record_offset = guard == Guard::BoundExec ? 672u : 3696u; - const uint32_t record_stride = guard == Guard::BoundExec ? 784u : 272u; + const bool exec_bound = guard != Guard::BoundDiamond; + const uint32_t record_offset = exec_bound ? 672u : 3696u; + const uint32_t record_stride = exec_bound ? 784u : 272u; for (uint32_t key = 0; key < 2u; ++key) { const auto word = (record_offset + key * record_stride) / 4u; records.words[word] = 0x500u + key; diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 064aaf0f9..e01c3bada 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -36569,8 +36569,11 @@ void CheckGuardedRecordImageTable(VulkanHarness &vulkan) { EncodeSop1(0x04, 126, 28)}); code.push_back(EncodeSopc(0x09, 22, 21)); if (masked) code.insert(code.end(), { + EncodeVopc(0xc2, 249, 22), // Mask the FF1 sentinel into s2:s3. + EncodeVopcSdwa(193, 2, 1, 6, 6, 0, 0, 0, 0, 0, 0, 1, 1), EncodeVop2(0x1c, 10, 253, 13), // SCC encoded as 0/1, OR per-lane done. EncodeVopc(0xc5, InlineU32(0), 10), + EncodeSop2(0x11, 106, 106, 2), EncodeSop2(0x15, 126, 28, 106)}); range_exit = code.size(); code.push_back(0u); } @@ -36621,7 +36624,8 @@ void CheckGuardedRecordImageTable(VulkanHarness &vulkan) { else test.opcodes.insert(test.opcodes.end(), {O::S_CMP_GE_U32, O::S_CBRANCH_SCC1, O::S_BITCMP1_B32}); if (masked) test.opcodes.insert(test.opcodes.end(), {O::S_CSELECT_B64, - O::V_OR_B32, O::V_CMP_NE_U32, O::S_ANDN2_B64, O::S_CBRANCH_EXECZ}); + O::V_CMP_EQ_U32, O::V_OR_B32, O::V_CMP_NE_U32, O::S_OR_B64, + O::S_ANDN2_B64, O::S_CBRANCH_EXECZ}); test.opcodes.insert(test.opcodes.end(), {O::S_MOV_B64, O::V_ADD_NC_U32}); auto compiled = CompileCase(test, vulkan.SubgroupSize()); const auto sampled_count = std::ranges::count_if(compiled.program.info.images,