From 92ac54a493aa1a5fa0891f4b35431d9c8932c022 Mon Sep 17 00:00:00 2001 From: nmzik Date: Sun, 4 Oct 2026 10:46:00 +0400 Subject: [PATCH] shader: eliminate impossible relative writes with closed loop bounds Co-authored-by: Ali Almohaya --- .../recompiler/ir/passes/ResourceTracking.cpp | 115 +++++++++++++++--- tests/ResourceTrackingTests.cpp | 87 +++++++++++++ 2 files changed, 188 insertions(+), 14 deletions(-) diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp index 528dfced2..e381f76b8 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp @@ -352,6 +352,7 @@ public: if (m_program.resource_tracking_complete) { Fail(0, "resources already tracked"); } + FoldBoundedLoopSelectors(); PlanScalarReads(); EliminateDeadCode(m_program.blocks); PlanIndirectDescriptors(); @@ -1409,12 +1410,13 @@ private: return true; } - const Inst* BoundedLoop(Value key, const Block* use) const { + const Inst* BoundedLoop(Value key, const Block* use, const auto& accepts_bound) const { const auto* phi = key.Resolve().TryInstruction(); if (phi == nullptr || phi->GetOpcode() != ValueOpcode::Phi || - phi->GetType() != Type::U32 || phi->NumArgs() != 2u || + phi->GetType() != Type::U32 || phi->NumArgs() != 2u || phi->NumPhiBlocks() != 2u || m_program.blocks.size() != m_program.block_info.size()) return {}; const Block* increment_block = nullptr; + uint32_t initial_arm = 0; for (uint32_t initial = 0; initial < 2u; ++initial) { const auto zero = phi->Arg(initial).Resolve(); const auto* step = phi->Arg(initial ^ 1u).Resolve().TryInstruction(); @@ -1427,6 +1429,7 @@ private: (step->Arg(1).Resolve() == key && ImmediateU32(step->Arg(0), increment) && increment == 1u)) { increment_block = step->Parent(); + initial_arm = initial; break; } } @@ -1437,8 +1440,8 @@ private: const auto* compare = use_of_key.user; if ((compare->GetOpcode() != ValueOpcode::SLessThan32 && compare->GetOpcode() != ValueOpcode::ULessThan32) || use_of_key.operand != 0u || - !ValidateRuntimeValue(m_program, compare->Arg(1), RuntimeValueType::Integer)) continue; - if (!GuardedOnEntry(use, stops, [&](const EdgePredicate& edge) { + !accepts_bound(*compare, initial_arm)) continue; + if (!GuardedOnEntry(use != nullptr ? use : increment_block, stops, [&](const EdgePredicate& edge) { return edge.positive && Implies(edge.condition, Value(use_of_key.user)); })) continue; LoopBoundProof proof(m_program, *phi, *compare); @@ -1451,6 +1454,54 @@ private: return {}; } + const Inst* BoundedLoop(Value key, const Block* use) const { + return BoundedLoop(key, use, [&](const Inst& compare, uint32_t) { + return ValidateRuntimeValue(m_program, compare.Arg(1), RuntimeValueType::Integer); + }); + } + + void FoldBoundedLoopSelectors() { + for (auto* header: m_program.blocks) { + for (auto& induction: *header) { + const Inst* carried = nullptr; + uint32_t maximum = 0; + const auto* compare = BoundedLoop(Value(&induction), nullptr, + [&](const Inst& test, uint32_t initial) { + if (test.GetOpcode() != ValueOpcode::ULessThan32) return false; + carried = test.Arg(1).Resolve().TryInstruction(); + if (carried == nullptr || carried->GetOpcode() != ValueOpcode::Phi || + carried->GetType() != Type::U32 || carried->Parent() != header || + carried->NumArgs() != 2u || carried->NumPhiBlocks() != 2u || + carried->PhiBlock(0) != induction.PhiBlock(0) || + carried->PhiBlock(1) != induction.PhiBlock(1)) return false; + maximum = SelectorMaximum(carried->Arg(initial)); + if (maximum == 0u || maximum == UINT32_MAX) return false; + const std::array assumptions {std::pair {static_cast(&induction), maximum - 1u}, + std::pair {carried, maximum}}; + // Close the carried-bound invariant before using either assumption. + return SelectorMaximum(carried->Arg(initial ^ 1u), assumptions) <= maximum; + }); + if (compare == nullptr) continue; + const std::array assumptions {std::pair {static_cast(&induction), maximum - 1u}, + std::pair {carried, maximum}}; + // Native scalar branches advance the zero/+1 induction uniformly, so + // even an any-lane bound test limits it by the largest carried bound. + // Per-lane edges instead require the comparison on every executing lane. + for (auto* block: m_program.blocks) { + if (!GuardedOnEntry(block, [&](const Block* at) { return at == header; }, + [&](const EdgePredicate& edge) { + return edge.positive && Implies(edge.condition, Value(const_cast(compare))); + })) continue; + for (auto& inst: *block) { + if (inst.GetOpcode() == ValueOpcode::SelectU32 && + SelectorPredicateFalse(inst.Arg(0), assumptions)) + inst.ReplaceUsesWith(inst.Arg(2)); + } + } + } + } + } + bool BoundedSelectedIndex(Value index, Value active, DescriptorSource::IndirectDescriptor& indirect) const { const auto* selected = index.Resolve().TryInstruction(); @@ -1728,20 +1779,56 @@ private: return true; } - uint32_t SelectorMaximum(Value value) const { + using SelectorBounds = std::span>; + + bool SelectorPredicateFalse(Value value, SelectorBounds bounds, uint32_t depth = 0) const { + if (depth > 32u) return false; + value = value.Resolve(); + if (value.IsImmediate()) return value.GetType() == Type::U1 && !value.U1(); + const auto* inst = value.TryInstruction(); + if (inst == nullptr) return false; + if (inst->GetOpcode() == ValueOpcode::LogicalAnd) + return SelectorPredicateFalse(inst->Arg(0), bounds, depth + 1u) || + SelectorPredicateFalse(inst->Arg(1), bounds, depth + 1u); + if (inst->GetOpcode() == ValueOpcode::IEqual32) { + for (uint32_t arg = 0; arg < 2u; ++arg) { + uint32_t target; + if (ImmediateU32(inst->Arg(arg), target) && + SelectorMaximum(inst->Arg(arg ^ 1u), bounds, depth + 1u) < target) return true; + } + } + return false; + } + + uint32_t SelectorMaximum(Value value, SelectorBounds bounds = {}, uint32_t depth = 0) const { + if (depth > 32u) return UINT32_MAX; value = value.Resolve(); if (value.IsImmediate() && value.GetType() == Type::U32) return value.U32(); const auto* inst = value.TryInstruction(); if (inst == nullptr) return UINT32_MAX; - if (inst->GetOpcode() == ValueOpcode::FindILsb32) - return NonzeroOnEntry(inst->Arg(0), inst->Parent()) ? 31u : UINT32_MAX; - if (inst->GetOpcode() == ValueOpcode::UMin32) - return std::min(SelectorMaximum(inst->Arg(0)), SelectorMaximum(inst->Arg(1))); - uint32_t shift; - if (inst->GetOpcode() == ValueOpcode::ShiftLeftLogical32 && - ImmediateU32(inst->Arg(1), shift) && shift < 32u) { - const auto maximum = uint64_t {SelectorMaximum(inst->Arg(0))} << shift; - if (maximum <= UINT32_MAX) return static_cast(maximum); + for (const auto& [phi, maximum]: bounds) + if (inst == phi) return maximum; + const auto maximum = [&](uint32_t arg) { + return SelectorMaximum(inst->Arg(arg), bounds, depth + 1u); + }; + switch (inst->GetOpcode()) { + case ValueOpcode::FindILsb32: + return NonzeroOnEntry(inst->Arg(0), inst->Parent()) ? 31u : UINT32_MAX; + case ValueOpcode::UMin32: + case ValueOpcode::BitwiseAnd32: return std::min(maximum(0), maximum(1)); + case ValueOpcode::SelectU32: + return SelectorPredicateFalse(inst->Arg(0), bounds, depth + 1u) + ? maximum(2) : std::max(maximum(1), maximum(2)); + case ValueOpcode::ShiftRightLogical32: + case ValueOpcode::ShiftLeftLogical32: { + uint32_t shift; + if (!ImmediateU32(inst->Arg(1), shift) || shift >= 32u) break; + if (inst->GetOpcode() == ValueOpcode::ShiftRightLogical32) return maximum(0) >> shift; + const auto result = uint64_t {maximum(0)} << shift; + if (result <= UINT32_MAX) return static_cast(result); + break; + } + default: break; } return UINT32_MAX; } diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index 85c3e4bd1..ba80e58cd 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -2728,6 +2728,92 @@ void TestInvariantLoopPhi() { "loop-invariant descriptor phi was not evaluated through typed SSA"); } +void TestBoundedRelativeRegisterWrites() { + namespace CFG = Libs::Graphics::ShaderRecompiler::CFG; + enum class Variant { Bounded, BoundClobber, DescriptorClobber, EntryBypass }; + for (const auto variant : {Variant::Bounded, Variant::BoundClobber, + Variant::DescriptorClobber, Variant::EntryBypass}) { + Fixture fixture; + auto *entry = fixture.block; + auto *header = fixture.AddBlock(); + auto *body = fixture.AddBlock(); + auto *exit = fixture.AddBlock(); + entry->AddBranch(header); + if (variant == Variant::EntryBypass) entry->AddBranch(body); + header->AddBranch(body); + header->AddBranch(exit); + body->AddBranch(header); + fixture.program.block_info[0].terminator = { + .kind = variant == Variant::EntryBypass ? CFG::TerminatorKind::ConditionalBranch + : CFG::TerminatorKind::Branch, + .true_block = 1u, .false_block = 2u}; + fixture.program.block_info[1].terminator = { + .kind = CFG::TerminatorKind::ConditionalBranch, + .true_block = 2u, .false_block = 3u}; + fixture.program.block_info[2].terminator = { + .kind = CFG::TerminatorKind::Branch, .true_block = 1u}; + fixture.program.block_info[3].terminator.kind = CFG::TerminatorKind::Return; + + const auto lane = fixture.Emit(ValueOpcode::GetBuiltin, + {Value(static_cast(StageInputKind::LocalInvocationId)), Value(0u)}); + const auto enabled = fixture.Emit(ValueOpcode::ULessThan32, {lane, Value(32u)}); + fixture.program.block_info[0].condition = enabled; + const auto chunk = fixture.Emit(ValueOpcode::SelectU32, + {enabled, Value(64u), Value(512u)}); + const auto initial_bound = fixture.Emit(ValueOpcode::ShiftRightLogical32, + {chunk, Value(6u)}); + const auto initial_records = fixture.UserData(2u); + const auto base = fixture.UserData(0u); + auto &counter = header->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32)); + auto &bound = header->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32)); + auto &records = header->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32)); + const auto in_range = fixture.Emit(ValueOpcode::ULessThan32, + {Value(&counter), Value(&bound)}, 0, header); + fixture.program.block_info[1].condition = fixture.Emit( + ValueOpcode::ConditionRef, {in_range}, CFG::BranchCondition::VccNonZero, header); + const auto m0 = fixture.Emit(ValueOpcode::BitwiseAnd32, + {fixture.Emit(ValueOpcode::ShiftLeftLogical32, + {Value(&counter), Value(1u)}, 0, body), Value(255u)}, 0, body); + const auto relative_write = [&](Value old_value, uint32_t offset) { + const auto selected = fixture.Emit(ValueOpcode::IEqual32, + {m0, Value(offset)}, 0, body); + const auto active = fixture.Emit(ValueOpcode::LogicalAnd, + {enabled, selected}, 0, body); + return fixture.Emit(ValueOpcode::SelectU32, {active, lane, old_value}, 0, body); + }; + bound.AddPhiOperand(entry, initial_bound); + bound.AddPhiOperand(body, relative_write(Value(&bound), + variant == Variant::BoundClobber ? 14u : 22u)); + records.AddPhiOperand(entry, initial_records); + records.AddPhiOperand(body, relative_write(Value(&records), + variant == Variant::DescriptorClobber ? 14u : 21u)); + counter.AddPhiOperand(entry, Value(0u)); + counter.AddPhiOperand(body, fixture.Emit(ValueOpcode::IAdd32, + {Value(&counter), Value(1u)}, 0, body)); + const auto handle = fixture.Emit(ValueOpcode::GetBufferResource, + {base, Value(4u << 16u), Value(&records), Value(0x16204u)}, + MemoryFlags{0, 0x3e8}, exit); + MemoryInfo memory; + memory.kind = ResourceKind::Buffer; + fixture.Emit(ValueOpcode::StoreBufferU32, + {handle, Value(0u), Value(0u), Value(0u), Value(1u), Value(true)}, + fixture.AddMemory(memory, 0x3e8), exit); + if (variant != Variant::Bounded) { + CheckFatal([&] { fixture.PlanAndTrack(); }, "not a valid runtime value", + "relative register proof discarded a possible loop clobber"); + continue; + } + fixture.PlanAndTrack(); + const std::array user_data{0x1000u, 0u, 72u}; + SrtRuntime runtime{.user_data = user_data}; + DescriptorValue descriptor; + Check(SrtWalker(fixture.program, runtime).EvaluateDescriptor( + fixture.program.info.buffers[0].source, descriptor) && + descriptor.dwords[2] == 72u, + "impossible relative writes left a false descriptor dependency"); + } +} + void TestDmaAddressMaterialization() { Fixture fixture; const auto based = @@ -3553,6 +3639,7 @@ int main() { Run("finite image bit scan sentinel", TestFiniteImageBitScanSentinel); Run("runtime-rooted loop", TestLoopCycleEnteredThroughRuntimeValue); Run("invariant loop phi", TestInvariantLoopPhi); + Run("bounded relative register writes", TestBoundedRelativeRegisterWrites); Run("DMA address materialization", TestDmaAddressMaterialization); Run("dynamic FLAT address", TestDynamicFlatAddressesUseDma); Run("buffer swizzle specialization", TestBufferSwizzleSpecialization);