From 3b1a94707a5d30cf88082a97bf1c160284b2459a Mon Sep 17 00:00:00 2001 From: nmzik Date: Fri, 9 Oct 2026 15:04:01 +0200 Subject: [PATCH] shader: capture only memory-dependent resource predicates --- .../ir/passes/ResourceMaterialization.cpp | 9 ++++- .../shader/recompiler/ir/passes/SrtWalker.cpp | 3 +- tests/ResourceMaterializationTests.cpp | 40 +++++++++++-------- tests/shaderCfgTests.cpp | 7 ++-- 4 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index cf467412b..a75113852 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -1007,10 +1007,15 @@ ResourcePlan ExtractResourcePlan(const Program& program) { // A proven uniform factor can decide a branch even when its other lanes are unknown. // Keep only that Boolean structure, never the varying shader dependency graph. Value unknown; + bool capture_control_reads = false; std::function ClonePredicate = [&](Value value) -> Value { value = value.Resolve(); if (value.IsEmpty()) return {}; - if (ValidateRuntimeValue(program, value, RuntimeValueType::Integer)) return Clone(value); + if (ValidateRuntimeValue(program, value, RuntimeValueType::Integer)) { + capture_control_reads |= + !ValidateRuntimeValue(program, value, RuntimeValueType::ImmutableInteger); + return Clone(value); + } const auto* inst = value.TryInstruction(); if (inst == nullptr) return {}; const auto op = inst->GetOpcode(); @@ -1040,7 +1045,7 @@ ResourcePlan ExtractResourcePlan(const Program& program) { for (uint32_t i = 0; i < plan.uniform_fill.fill.words; ++i) { plan.uniform_fill.values[i] = Clone(plan.uniform_fill.values[i]); } - plan.capture_specialization_reads |= capture_indirect_reads || !plan.control_flow.empty(); + plan.capture_specialization_reads |= capture_indirect_reads || capture_control_reads; if (plan.capture_specialization_reads) { // Every former flat alias now targets its retained raw read. Mark those reads // on the normalized graph, so discarded Phi/EXEC dependencies stay discarded. diff --git a/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp b/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp index ca294b421..492ab303c 100644 --- a/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp +++ b/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp @@ -966,7 +966,8 @@ bool SrtWalker::RefreshFlatBuffer(std::vector& flat) { } uint32_t condition = 0; auto& predicate = m_clean_evaluator != nullptr ? *m_clean_evaluator : *this; - if (!block.condition.IsEmpty() && m_runtime.read_specialization_memory != nullptr && + if (!block.condition.IsEmpty() && + (m_runtime.read_specialization_memory != nullptr || !m_program.capture_specialization_reads) && predicate.Evaluate(block.condition, condition)) { pending.push_back(block.successors[condition != 0u ? 0u : 1u]); } else { diff --git a/tests/ResourceMaterializationTests.cpp b/tests/ResourceMaterializationTests.cpp index 2688062ef..be17c560d 100644 --- a/tests/ResourceMaterializationTests.cpp +++ b/tests/ResourceMaterializationTests.cpp @@ -435,7 +435,7 @@ void TestUnbasedFlatCacheHitMaterializes() { "unbased FLAT plan produced unexpected descriptors"); } -void TestWrittenDescriptorUsesStrictReaderOnce() { +void TestWrittenDescriptorPredicateReads(bool memory_condition) { using namespace Libs::Graphics::ShaderRecompiler::IR; Program program; program.stage = Libs::Graphics::ShaderType::Compute; @@ -456,10 +456,10 @@ void TestWrittenDescriptorUsesStrictReaderOnce() { source.dword_count = 4; program.descriptor_sources.push_back(source); program.info.buffers.push_back({.source = 0, .written = true}); - // A host-evaluable branch captures resource reads and needs the writable - // descriptor's clean provenance for the renderer's disjointness proof. + // Only memory-derived host decisions need clean, disjoint descriptor reads. auto &condition = block.AppendNewInst(ValueOpcode::IEqual32, - {Value(&offset), Value(4u)}); + {memory_condition ? Value(&read) : Value(&offset), + Value(memory_condition ? 0x8000u : 4u)}); auto &store_block = AddValueBlock(program); AddValueBlock(program); program.blocks[0]->condition = Value(&condition); @@ -480,8 +480,8 @@ void TestWrittenDescriptorUsesStrictReaderOnce() { {Value(&output), Value(0u), Value(0u), Value(0u), Value(1u), Value(true)}) .SetFlags(MemoryFlags{.index = 1}); auto plan = ExtractResourcePlan(program); - Check(plan.capture_specialization_reads, - "conditional writable descriptor lost its alias proof"); + Check(plan.capture_specialization_reads == memory_condition && !plan.control_flow.empty(), + "conditional writable descriptor used the wrong memory dependency policy"); struct Reads { uint32_t ordinary = 0; uint32_t strict = 0; bool clean = false; } reads; const std::array user_data{4u}; const SrtRuntime runtime{ @@ -501,16 +501,21 @@ void TestWrittenDescriptorUsesStrictReaderOnce() { }}; ResourceSnapshot snapshot; ResourceSpecialization specialization; - Check(!MaterializeResources(plan, runtime, snapshot, specialization) && - reads.ordinary == 0 && reads.strict == 1, - "GPU-dirty dynamic writable descriptor bypassed strict provenance"); - reads.clean = true; - reads.strict = 0; - Check(MaterializeResources(plan, runtime, snapshot, specialization) && - reads.ordinary == 0 && reads.strict == 1 && snapshot.buffers[0].dwords[0] == 0x8000u && - snapshot.specialization_reads == - std::vector>{{0x1004u, 4u}}, - "writable descriptor was evaluated twice or scalar EXEC suppressed its read"); + for (const bool clean : {false, true}) { + reads = {.clean = clean}; + // A failed predicate explores both edges and the writable descriptor retries its clean read. + const bool expected = !memory_condition || clean; + Check(MaterializeResources(plan, runtime, snapshot, specialization) == expected && + reads.ordinary == (memory_condition ? 0u : 1u) && + reads.strict == (memory_condition ? (clean ? 1u : 2u) : 0u), + "writable descriptor changed reader policy or bypassed a dirty memory predicate"); + if (!expected) continue; + Check(snapshot.buffers[0].dwords[0] == 0x8000u && + snapshot.specialization_reads == (memory_condition + ? std::vector>{{0x1004u, 4u}} + : std::vector>{}), + "writable descriptor lost its address or captured an immutable predicate"); + } } void TestFailedMaterializationRejectsStage() { @@ -671,7 +676,8 @@ int main() { TestUniformVectorDescriptorRead(); TestExactReciprocalDescriptorArithmetic(); TestUnbasedFlatCacheHitMaterializes(); - TestWrittenDescriptorUsesStrictReaderOnce(); + for (const bool memory_condition : {false, true}) + TestWrittenDescriptorPredicateReads(memory_condition); TestFailedMaterializationRejectsStage(); TestFiniteImageRefreshReusesScalarReads(); TestMixedSamplerVariantsShareRuntimeDescriptor(); diff --git a/tests/shaderCfgTests.cpp b/tests/shaderCfgTests.cpp index ec1c23051..63e19c398 100644 --- a/tests/shaderCfgTests.cpp +++ b/tests/shaderCfgTests.cpp @@ -13779,7 +13779,7 @@ void TestUniformSelectedDescriptorLoadAddress() { auto translated = ShaderRecompiler::TranslateProgram(shader, options); auto plan = ExtractResourcePlan(translated.program); Check(plan.srt_reads.size() == 4u && plan.info.buffers.size() == 1u && - plan.info.buffers[0].written, + plan.info.buffers[0].written && !plan.capture_specialization_reads, "conditional scalar address duplicated or detached descriptor reads"); const auto read = +[](void *data, uint64_t address, std::span words) { const auto flag = *static_cast(data); @@ -13792,13 +13792,14 @@ void TestUniformSelectedDescriptorLoadAddress() { return true; }; const SrtRuntime runtime{.user_data = user_data, .read_memory = read, - .userdata = &user_data[0], .read_specialization_memory = read}; + .userdata = &user_data[0]}; ResourceSnapshot snapshot; ResourceSpecialization specialization; for (const uint32_t flag : {0u, 1u, 0u}) { user_data[0] = flag; Check(MaterializeResources(plan, runtime, snapshot, specialization) && - snapshot.buffers[0].dwords[0] == 0x3000u + flag * 0x1000u, + snapshot.buffers[0].dwords[0] == 0x3000u + flag * 0x1000u && + snapshot.specialization_reads.empty(), "conditional descriptor load read the untaken pointer"); } }