From 5d1ad31c4907141267d98a048cb96d0c65cddb68 Mon Sep 17 00:00:00 2001 From: nmzik Date: Wed, 23 Sep 2026 10:47:08 +0400 Subject: [PATCH] Resolve guarded descriptor Phis at their use edge Co-authored-by: Techx3 <56268269+Techx3@users.noreply.github.com> Co-authored-by: GodsWill --- .../recompiler/ir/passes/ResourceTracking.cpp | 52 ++++++++++-- tests/ResourceTrackingTests.cpp | 85 +++++++++++++++++++ 2 files changed, 129 insertions(+), 8 deletions(-) diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp index 409ce712c..c9b517b51 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp @@ -169,7 +169,7 @@ private: std::abort(); } - Value LowerDescriptorPhi(Value value) { + Value LowerDescriptorPhi(Value value, const Block* use) { value = value.Resolve(); const auto* phi = value.TryInstruction(); if (m_shader_writes || phi == nullptr || phi->GetOpcode() != ValueOpcode::Phi || @@ -177,17 +177,53 @@ private: m_program.blocks.size() != m_program.block_info.size()) { return value; } - for (const auto& [original, selected]: m_descriptor_selections) { - if (original == phi) { - return selected; - } - } - const auto* merge = phi->Parent(); + const auto* merge = phi->Parent(); const auto* branch = phi->PhiBlock(0); if (merge == nullptr || branch == nullptr || phi->PhiBlock(1) == nullptr || branch == phi->PhiBlock(1)) { return value; } + // Structurization can merge the descriptor and its use predicate in parallel Phis. + // Match their incoming blocks to exclude only edges that cannot reach this use. + if (use != nullptr && use->ImmPredecessors().size() == 1u && + use->ImmPredecessors()[0] == merge) { + const auto merge_it = std::ranges::find(m_program.blocks, merge); + const auto use_it = std::ranges::find(m_program.blocks, use); + if (merge_it != m_program.blocks.end() && use_it != m_program.blocks.end()) { + const auto& info = m_program.block_info[merge_it - m_program.blocks.begin()]; + const auto& term = info.terminator; + const auto id = m_program.block_info[use_it - m_program.blocks.begin()].id; + const auto* condition = info.condition.Resolve().TryInstruction(); + if (term.kind == CFG::TerminatorKind::ConditionalBranch && + term.true_block != term.false_block && + (id == term.true_block || id == term.false_block) && condition != nullptr && + condition->GetOpcode() == ValueOpcode::Phi && condition->GetType() == Type::U1 && + condition->Parent() == merge && condition->NumArgs() == 2u && + condition->NumPhiBlocks() == 2u) { + const bool taken = id == term.true_block; + for (uint32_t skipped = 0; skipped < 2u; skipped++) { + const auto excluded = condition->Arg(skipped).Resolve(); + const auto included = condition->Arg(skipped ^ 1u).Resolve(); + if (!excluded.IsImmediate() || excluded.GetType() != Type::U1 || + excluded.U1() == taken || + (included.IsImmediate() && included.U1() != taken)) { + continue; + } + for (uint32_t selected = 0; selected < 2u; selected++) { + if (phi->PhiBlock(selected) == condition->PhiBlock(skipped ^ 1u) && + phi->PhiBlock(selected ^ 1u) == condition->PhiBlock(skipped)) { + return phi->Arg(selected); + } + } + } + } + } + } + for (const auto& [original, selected]: m_descriptor_selections) { + if (original == phi) { + return selected; + } + } if (branch->ImmSuccessors().size() != 2u) { if (branch->ImmPredecessors().size() != 1u) { return value; @@ -247,7 +283,7 @@ private: } descriptor.dword_count = width; for (uint32_t i = 0; i < width; i++) { - descriptor.dwords[i] = LowerDescriptorPhi(handle.Arg(i)); + descriptor.dwords[i] = LowerDescriptorPhi(handle.Arg(i), handle.Parent()); } if (sample_adjust) { descriptor.dwords[3] = CanonicalizeSampleAdjustDword3(descriptor.dwords[3]); diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index 16bdb3882..a1a450cb2 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -1463,6 +1463,90 @@ void TestConditionalSamplerPhi() { } } +void TestGuardedSamplerPhi() { + namespace CFG = Libs::Graphics::ShaderRecompiler::CFG; + const auto make_plan = [](bool take_true, bool reverse_phi, bool bypass, + bool mismatched, bool ambiguous) { + Fixture fixture(ShaderType::Pixel); + auto *entry = fixture.block; + auto *loaded = fixture.AddBlock(); + auto *merge = fixture.AddBlock(); + auto *sample = fixture.AddBlock(); + auto *exit = fixture.AddBlock(); + entry->AddBranch(loaded); + entry->AddBranch(merge); + loaded->AddBranch(merge); + merge->AddBranch(sample); + merge->AddBranch(exit); + sample->AddBranch(exit); + if (bypass) exit->AddBranch(sample); + const auto lane = fixture.Emit(ValueOpcode::LaneId); + const auto predicate = fixture.Emit(ValueOpcode::IEqual32, {lane, Value(0u)}); + fixture.program.block_info[0].condition = predicate; + fixture.program.block_info[0].terminator = { + .kind = CFG::TerminatorKind::ConditionalBranch, + .true_block = 1u, .false_block = 2u}; + fixture.program.block_info[1].terminator = { + .kind = CFG::TerminatorKind::Branch, .true_block = 2u}; + auto &guard = merge->AppendNewInst(ValueOpcode::Phi, {}, + static_cast(Type::U1)); + guard.AddPhiOperand(entry, ambiguous ? predicate : Value(!take_true)); + guard.AddPhiOperand(mismatched ? sample : loaded, predicate); + fixture.program.block_info[2].condition = Value(&guard); + fixture.program.block_info[2].terminator = { + .kind = CFG::TerminatorKind::ConditionalBranch, + .true_block = take_true ? 3u : 4u, + .false_block = take_true ? 4u : 3u}; + fixture.program.block_info[3].terminator = { + .kind = CFG::TerminatorKind::Branch, .true_block = 4u}; + fixture.program.block_info[4].terminator = { + .kind = bypass ? CFG::TerminatorKind::Branch : CFG::TerminatorKind::Return, + .true_block = 3u}; + std::array words; + for (uint32_t word = 0; word < words.size(); ++word) { + // An arbitrary excluded value proves this is control-flow reasoning, not null filtering. + const auto first = Value(0xbad000u + word); + const auto second = fixture.UserData(word); + auto &phi = merge->AppendNewInst(ValueOpcode::Phi, {}, + static_cast(Type::U32)); + phi.AddPhiOperand(reverse_phi ? loaded : entry, reverse_phi ? second : first); + phi.AddPhiOperand(reverse_phi ? entry : loaded, reverse_phi ? first : second); + words[word] = Value(&phi); + } + fixture.block = sample; + const auto image = fixture.Image({Value(0u), Value(0u), Value(0u), Value(0u), + Value(0u), Value(0u), Value(0u), Value(0u)}); + const auto sampler = fixture.Sampler(words); + MemoryInfo memory; + memory.kind = ResourceKind::Image; + memory.image_dimension = Decoder::ImageDimension::Dim2D; + fixture.Emit(ValueOpcode::ImageSampleRaw, + {image, sampler, fixture.ImageAddress()}, + fixture.AddMemory(memory, 0x2a0)); + fixture.PlanAndTrack(); + Check(words[0].ResolveInstruction()->GetOpcode() == ValueOpcode::Phi, + "guarded host descriptor selection rewrote the GPU Phi"); + return ExtractResourcePlan(fixture.program); + }; + for (const bool take_true : {false, true}) { + for (const bool reverse_phi : {false, true}) { + auto plan = make_plan(take_true, reverse_phi, false, false, false); + const std::array user_data{0x444u, 0x555u, 0x666u, 0x777u}; + DescriptorValue selected; + Check(SrtWalker(plan, {.user_data = user_data}).EvaluateDescriptor( + plan.info.samplers[0].source, selected) && + std::equal(user_data.begin(), user_data.end(), selected.dwords.begin()), + "guarded sampler did not match descriptor and predicate predecessors"); + } + } + CheckFatal([&] { make_plan(true, false, true, false, false); }, + "not a valid runtime value", "sampler guard accepted an unguarded path"); + CheckFatal([&] { make_plan(true, false, false, true, false); }, + "not a valid runtime value", "sampler guard ignored predecessor identity"); + CheckFatal([&] { make_plan(true, false, false, false, true); }, + "not a valid runtime value", "sampler guard discarded a reachable alternative"); +} + void TestLoopCycleEnteredThroughRuntimeValue() { Fixture fixture; auto *entry = fixture.block; @@ -2119,6 +2203,7 @@ int main() { Run("dynamic SRT", TestDynamicSrtReadRemainsExplicit); Run("phi validation", TestPhiValidation); Run("conditional sampler phi", TestConditionalSamplerPhi); + Run("guarded sampler phi", TestGuardedSamplerPhi); Run("runtime-rooted loop", TestLoopCycleEnteredThroughRuntimeValue); Run("invariant loop phi", TestInvariantLoopPhi); Run("DMA address materialization", TestDmaAddressMaterialization);