From 25e1cc7ec940090755abf8a7549d9086c3f1a9d7 Mon Sep 17 00:00:00 2001 From: nmzik Date: Tue, 13 Oct 2026 00:12:55 +0200 Subject: [PATCH] shader: preserve descriptor bounds through guarded branch joins --- .../recompiler/ir/passes/ResourceTracking.cpp | 42 +++++++++---------- tests/ResourceTrackingTests.cpp | 41 ++++++++++++++---- tests/ShaderRecompilerComputeTests.cpp | 26 ++++++++---- 3 files changed, 71 insertions(+), 38 deletions(-) diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp index 1210998d1..f850de536 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp @@ -1030,23 +1030,13 @@ private: LaneQuantifier lanes = LaneQuantifier::All; }; - bool HasEntryGuard(const Block* block, const auto& accepts, const Block* definition = nullptr) const { - // Each unique predecessor must execute before this use. Stop at joins and - // definitions so a guard from an earlier iteration cannot bound a new value. - for (size_t depth = 0; block != nullptr && depth < m_program.blocks.size(); ++depth) { - if (block == definition || block->ImmPredecessors().size() != 1u) { - return false; - } - const auto* previous = block->ImmPredecessors()[0]; - const auto edge = ConditionalEdge(previous, block); - if (edge && edge->lanes == LaneQuantifier::All && accepts(*edge)) return true; - block = previous; - } - return false; - } - bool NonzeroOnEntry(Value value, const Block* block) const { - return HasEntryGuard(block, [&](const EdgePredicate& edge) { + if (block == nullptr) return false; + bool witnessed = false; + const bool guarded = GuardedOnEntry(block, [](const Block* current) { + return current->ImmPredecessors().size() != 1u; + }, [&](const EdgePredicate& edge) { + if (edge.lanes != LaneQuantifier::All) return false; const auto* test = edge.condition.TryInstruction(); if (test != nullptr && test->NumArgs() == 2u && ((test->GetOpcode() == ValueOpcode::INotEqual32 && edge.positive) || @@ -1054,11 +1044,12 @@ private: for (uint32_t arg = 0; arg < 2u; ++arg) { uint32_t immediate; if (ImmediateU32(test->Arg(arg), immediate) && immediate == 0u && - EquivalentValue(m_program, test->Arg(arg ^ 1u), value)) return true; + EquivalentValue(m_program, test->Arg(arg ^ 1u), value)) return witnessed = true; } } return false; }); + return guarded && witnessed; } uint32_t WorkgroupAxis(Value key) const { @@ -1679,11 +1670,18 @@ private: Value UnsignedBoundOnEntry(Value key, const Block* block) const { const auto* definition = key.Resolve().TryInstruction(); Value bound; - const bool found = HasEntryGuard(block, [&](const EdgePredicate& edge) { - bound = UnsignedBound(edge.condition, key, edge.positive); - return !bound.IsEmpty() && ValidateRuntimeValue(m_program, bound, RuntimeValueType::Integer); - }, definition != nullptr ? definition->Parent() : nullptr); - return found ? bound : Value {}; + const bool guarded = GuardedOnEntry(block, [&](const Block* current) { + return definition != nullptr && current == definition->Parent(); + }, [&](const EdgePredicate& edge) { + if (edge.lanes != LaneQuantifier::All) return false; + const auto candidate = UnsignedBound(edge.condition, key, edge.positive); + if (candidate.IsEmpty() || + !ValidateRuntimeValue(m_program, candidate, RuntimeValueType::Integer)) return false; + if (bound.IsEmpty()) bound = candidate; + // Every incoming path must prove the same bound for this key definition. + return EquivalentValue(m_program, candidate, bound); + }); + return guarded ? bound : Value {}; } Value SharedLoadBound(Value value, Value active) { diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index 48a5e636d..d1bced8d5 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -563,7 +563,8 @@ void TestGuardedDirectImageTable() { namespace CFG = Libs::Graphics::ShaderRecompiler::CFG; enum class Guard { Nonzero, SccNonZero, Plain, Zero, Unrelated, Bypass, ExecZero, VccZero, - Bounded, BoundMissing, BoundUnrelated, BoundBypass, BoundAnyLane + Bounded, BoundMissing, BoundUnrelated, BoundBypass, BoundAnyLane, + BoundDiamond, BoundBranchSpecific, BoundMismatched }; const auto make_plan = [](Guard guard) { const bool bounded = guard >= Guard::Bounded; @@ -575,12 +576,6 @@ void TestGuardedDirectImageTable() { auto *before_sample = fixture.AddBlock(); auto *sample = fixture.AddBlock(); auto *exit = fixture.AddBlock(); - entry->AddBranch(middle); - entry->AddBranch(exit); - middle->AddBranch(before_sample); - before_sample->AddBranch(sample); - sample->AddBranch(exit); - if (bypass) exit->AddBranch(sample); const auto varying = bounded ? fixture.Emit(ValueOpcode::GetBuiltin, {Value(static_cast(StageInputKind::LocalInvocationId)), Value(0u)}) @@ -631,6 +626,32 @@ void TestGuardedDirectImageTable() { fixture.program.blocks[4]->terminator = { .kind = bypass ? CFG::TerminatorKind::Branch : CFG::TerminatorKind::Return, .true_block = fixture.program.blocks[3u]}; + if (guard == Guard::BoundDiamond) { + middle->condition = fixture.Emit(ValueOpcode::IEqual32, + {mask, Value(1u)}); + middle->terminator = {.kind = CFG::TerminatorKind::ConditionalBranch, + .true_block = before_sample, .false_block = sample}; + } else if (guard == Guard::BoundBranchSpecific || guard == Guard::BoundMismatched) { + entry->condition = fixture.Emit(ValueOpcode::IEqual32, + {mask, Value(1u)}); + entry->terminator = {.kind = CFG::TerminatorKind::ConditionalBranch, + .true_block = middle, .false_block = before_sample}; + middle->condition = condition; + middle->terminator = {.kind = CFG::TerminatorKind::ConditionalBranch, + .true_block = exit, .false_block = sample}; + if (guard == Guard::BoundMismatched) { + before_sample->condition = fixture.Emit(ValueOpcode::UGreaterThanEqual32, + {key, fixture.UserData(2)}); + before_sample->terminator = {.kind = CFG::TerminatorKind::ConditionalBranch, + .true_block = exit, .false_block = sample}; + } + } + for (auto *block : fixture.program.blocks) { + if (block->terminator.kind == CFG::TerminatorKind::Return) continue; + block->AddBranch(block->terminator.true_block); + if (block->terminator.kind == CFG::TerminatorKind::ConditionalBranch) + block->AddBranch(block->terminator.false_block); + } std::array pointer; for (uint32_t word = 0; word < pointer.size(); ++word) { if (bounded) { @@ -702,7 +723,8 @@ void TestGuardedDirectImageTable() { make_plan(Guard::Plain); for (const auto guard : {Guard::Zero, Guard::Unrelated, Guard::Bypass, Guard::ExecZero, Guard::VccZero, Guard::BoundMissing, - Guard::BoundUnrelated, Guard::BoundBypass, Guard::BoundAnyLane}) { + Guard::BoundUnrelated, Guard::BoundBypass, Guard::BoundAnyLane, + Guard::BoundBranchSpecific, Guard::BoundMismatched}) { CheckFatal([&] { make_plan(guard); }, "not a valid runtime value", "direct table accepted a selector without a dominating range guard"); } @@ -779,7 +801,8 @@ void TestGuardedDirectImageTable() { endpoint.watched_reads == 0u, "batched descriptor read crossed the 48-bit endpoint"); - auto bounded = make_plan(Guard::Bounded); + make_plan(Guard::Bounded); + auto bounded = make_plan(Guard::BoundDiamond); Check(bounded.capture_specialization_reads && bounded.info.images[1].written, "bounded table with image writes omitted specialization read capture"); LinearTestMemory records; diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index a609713b2..056e028e4 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -36419,12 +36419,14 @@ void CheckGuardedRecordImageTable(VulkanHarness &vulkan) { test.name = wave == 32u ? "GuardedRecordImageTable32" : "GuardedRecordImageTable64"; test.initial.resize((table + offset + stride * count) / 4u, 0xdeadbeefu); test.initial[table / 4u + 1u] = count; - for (u32 row = 0; row < count; ++row) { + for (u32 row = 0; row < count; ++row) + for (const u32 member : {0u, 1u}) { auto image = MakeSampledTextureData(Prospero::BufferFormat::k32Float); - image[0] = row + 1u; + image[0] = (row + 1u) * (member == 0u ? 1u : 10u); image[3] = DstSel(4, 0, 0, 1) | (static_cast(Prospero::ImageType::kColor2D) << 28u); - std::copy_n(image.begin(), 8, test.initial.begin() + (table + offset + row * stride) / 4u); + std::copy_n(image.begin(), 8, + test.initial.begin() + (table + offset + row * stride + member * 32u) / 4u); } test.user_data[0] = table; auto destination = MakeSampledTextureData(Prospero::BufferFormat::k32Float); @@ -36454,6 +36456,15 @@ void CheckGuardedRecordImageTable(VulkanHarness &vulkan) { const auto range_exit = code.size(); code.push_back(0u); code.insert(code.end(), {EncodeSop2(0x26, 23, 22, 255), stride, EncodeSop2(0x02, 23, 23, 255), offset, + EncodeSopc(0x0d, 22, InlineU32(0))}); + const auto skip_first = code.size(); code.push_back(0u); + code.insert(code.end(), { + EncodeSmem0(0x03, 8), EncodeSmem1(0, 23), + EncodeMimg0(0x27, 1), EncodeMimg1(4, 20, 2, 10), + EncodeVop2(0x03, 0, Vgpr(4), 0)}); + code[skip_first] = EncodeSopp(0x05, code.size() - skip_first - 1u); + // The second descriptor is reached through both sides of the optional first sample. + code.insert(code.end(), {EncodeSop2(0x02, 23, 23, InlineU32(32)), EncodeSmem0(0x03, 8), EncodeSmem1(0, 23), EncodeMimg0(0x27, 1), EncodeMimg1(4, 20, 2, 10), EncodeVop2(0x03, 0, Vgpr(4), 0), @@ -36468,7 +36479,7 @@ void CheckGuardedRecordImageTable(VulkanHarness &vulkan) { AppendEnd(&code); test.opcodes = {O::S_LSHR_B32, O::S_LOAD_DWORD, O::V_MOV_B32, O::S_FF1_I32_B32, O::S_CMP_LG_U32, O::S_CMP_GE_U32, O::S_CBRANCH_SCC0, O::S_CBRANCH_SCC1, - O::S_MUL_I32, O::S_ADD_I32, O::S_LOAD_DWORDX8, O::IMAGE_SAMPLE, + O::S_MUL_I32, O::S_ADD_I32, O::S_BITCMP1_B32, O::S_LOAD_DWORDX8, O::IMAGE_SAMPLE, O::V_ADD_F32, O::S_LSHL_B32, O::S_ANDN2_B32, O::S_BRANCH, O::IMAGE_STORE, O::S_ENDPGM}; auto compiled = CompileCase(test, vulkan.SubgroupSize()); @@ -36476,7 +36487,8 @@ void CheckGuardedRecordImageTable(VulkanHarness &vulkan) { [](const auto &image) { return image.resource_class == ShaderRecompiler::IR::ImageResourceClass::Sampled; }); - Require(test.name, "bounded descriptor rows", sampled_count == std::max(1u, count), + Require(test.name, "bounded descriptor rows", + count == 0u ? sampled_count <= 2u : sampled_count == count * 2u, "the runtime count lost a row or probed record padding"); std::vector images(compiled.program.info.images.size()); for (u32 resource = 0; resource < images.size(); ++resource) { @@ -36494,8 +36506,8 @@ void CheckGuardedRecordImageTable(VulkanHarness &vulkan) { auto sampler = vulkan.CreateSampler(test.name, 1); vulkan.Dispatch(test, compiled, buffer, nullptr, nullptr, &storage, nullptr, sampler, images); const std::vector expected = count == 0 ? std::vector(4, 0u) : - std::vector{std::bit_cast(count == 3 ? 4.0f : 1.0f), - std::bit_cast(2.0f), std::bit_cast(1.0f), 0u}; + std::vector{std::bit_cast(count == 3 ? 44.0f : 11.0f), + std::bit_cast(20.0f), std::bit_cast(11.0f), 0u}; CompareWords(test, "GPU worklist, guarded count and image writes", expected, vulkan.ReadImage(test.name, &storage)); vulkan.DestroyBuffer(&buffer);