From fe6499b86e8f4998975325e9341bf819bae6d0d3 Mon Sep 17 00:00:00 2001 From: nmzik Date: Tue, 13 Oct 2026 05:44:07 +0200 Subject: [PATCH] shader: preserve clamped scalar material selector provenance --- src/graphics/shader/recompiler/ir/ShaderIR.h | 1 + .../ir/passes/ResourceMaterialization.cpp | 2 ++ .../recompiler/ir/passes/ResourceTracking.cpp | 8 ++++- tests/ResourceTrackingTests.cpp | 29 ++++++++++++++++++- 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/graphics/shader/recompiler/ir/ShaderIR.h b/src/graphics/shader/recompiler/ir/ShaderIR.h index dedd6fbf5..4d80b5307 100644 --- a/src/graphics/shader/recompiler/ir/ShaderIR.h +++ b/src/graphics/shader/recompiler/ir/ShaderIR.h @@ -513,6 +513,7 @@ struct DescriptorSource { uint32_t source = UINT32_MAX; uint32_t stride = 0; uint32_t offset = 0; + uint32_t maximum = UINT32_MAX; // Four DWORD bitmap words per record; set-bit ranks subtract from the last key. bool bitset = false; std::optional rows; diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index 615f12d82..ff30c3aa6 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -538,6 +538,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p if (count < domain_count) keys.push_back(0u); // An out-of-bounds scalar read returns zero. } if (selector != nullptr && selector->byte_ranges.empty()) { + if (selector->maximum != UINT32_MAX) + for (auto& key: keys) key = std::min(key, selector->maximum); std::ranges::sort(keys); keys.erase(std::unique(keys.begin(), keys.end()), keys.end()); } diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp index 5baadd3b5..c0081bf18 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp @@ -2044,6 +2044,12 @@ private: bool MatchScalarMaterialKey(Value key, DescriptorSource::IndirectDescriptor& indirect, DescriptorSource& material_source) { const auto* read = key.Resolve().TryInstruction(); + uint32_t maximum = UINT32_MAX; + if (read != nullptr && read->GetOpcode() == ValueOpcode::UMin32) { + if (ImmediateU32(read->Arg(1), maximum)) read = read->Arg(0).Resolve().TryInstruction(); + else if (ImmediateU32(read->Arg(0), maximum)) read = read->Arg(1).Resolve().TryInstruction(); + else return false; + } if (read == nullptr) return false; uint32_t memory_index = 0; const auto* memory = ScalarReadMemory(*read, memory_index); @@ -2093,7 +2099,7 @@ private: if (!MakeRuntimeTableSource(*read, material_source)) return false; indirect.selector.emplace(DescriptorSource::IndirectDescriptor::SelectorRead { .source = InternSource(material_source), .stride = stride, - .offset = offset + (memory->offset & ~3u)}); + .offset = offset + (memory->offset & ~3u), .maximum = maximum}); indirect.key_count = count; if (rows) indirect.selector->rows.emplace( DescriptorSource::IndirectDescriptor::SelectorRead::CountedRows { diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index 57e56eaa0..4101c6cca 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -1057,6 +1057,7 @@ void TestGuardedDirectImageTable() { void TestBoundedComputeImageLoop() { namespace CFG = Libs::Graphics::ShaderRecompiler::CFG; + enum class Clamp { None, Right, Left, Dynamic }; enum class Variant { Bounded, Plain, Nonzero, TrueEdge, WrongGuard, EntryBypass, ExitBypass, GuardBlock, WrongStep, DivergentBound, DivergentKey, Disjunction, WrongPolarity, @@ -1065,7 +1066,8 @@ void TestBoundedComputeImageLoop() { Masked, MaskedWrongGuard, MaskResurrection, StatusOverwrite, GuardedDiamond, GuardedDiamondBypass }; - const auto make_plan = [](Variant variant, bool material = false, uint32_t first = 0u) { + const auto make_plan = [](Variant variant, bool material = false, uint32_t first = 0u, + Clamp clamp = Clamp::None) { const bool diamond = variant >= Variant::GuardedDiamond; Fixture fixture(diamond ? ShaderType::Vertex : ShaderType::Compute); fixture.program.wave_size = 64u; @@ -1300,6 +1302,12 @@ void TestBoundedComputeImageLoop() { image_key = fixture.Emit(ValueOpcode::LoadAddressU32, {table, fixture.Emit(ValueOpcode::IMul32, {key, Value(48u)}), Value(0u), Value(true)}, fixture.AddMemory(memory, 0x2ff0)); + if (clamp != Clamp::None) { + const auto maximum = clamp == Clamp::Dynamic ? fixture.UserData(3u) : Value(2u); + image_key = fixture.Emit(ValueOpcode::UMin32, + clamp == Clamp::Left ? std::initializer_list{maximum, image_key} + : std::initializer_list{image_key, maximum}); + } } const auto offset = fixture.Emit(ValueOpcode::IAdd32, {fixture.Emit(ValueOpcode::ShiftLeftLogical32, {image_key, Value(5u)}), @@ -1348,6 +1356,7 @@ void TestBoundedComputeImageLoop() { "nonzero-start image range retained descriptor-only scalar loads"); if (material) Check(indirect->selector->stride == 48u && indirect->selector->offset == 2756u && + indirect->selector->maximum == (clamp == Clamp::None ? UINT32_MAX : 2u) && fixture.program.descriptor_sources[indirect->selector->source].dword_count == 2u, "bounded scalar selector lost its pointer or record layout"); return ExtractResourcePlan(fixture.program); @@ -1502,6 +1511,24 @@ void TestBoundedComputeImageLoop() { memory.words[(first_key - memory.base) / 4u] = UINT32_MAX - 1000u; Check(!MaterializeResources(material, runtime, snapshot, specialization), "raw pointer selector silently discarded an invalid negative key"); + + for (const auto clamp : {Clamp::Right, Clamp::Left}) { + const auto clamped = make_plan(Variant::Bounded, true, 0u, clamp); + memory.words[(table - memory.base) / 4u + 8u + 3u] = + static_cast(Libs::Graphics::Prospero::ImageType::kColor3D) << 28u; + memory.words[(first_key - memory.base) / 4u] = 0u; + memory.words[(first_key + 48u - memory.base) / 4u] = UINT32_MAX; + memory.words[(first_key + 96u - memory.base) / 4u] = 2u; + user_data[2] = 3u; + memory.fail_address = first_key + 144u; + memory.descriptor_reads = 0u; + Check(MaterializeResources(clamped, runtime, snapshot, specialization) && + memory.descriptor_reads == 2u && snapshot.images.size() == 2u && + snapshot.images[0].dwords[0] == 0x100u && snapshot.images[1].dwords[0] == 0x102u, + "clamped scalar selector read an unreachable image or failed to deduplicate clamped keys"); + } + CheckFatal([&] { make_plan(Variant::Bounded, true, 0u, Clamp::Dynamic); }, + "not a valid runtime value", "scalar selector accepted an unproved dynamic clamp"); } void TestDescendingImageRange() {