From 6309ac838db4514e913f2a4616d859799199061b Mon Sep 17 00:00:00 2001 From: nmzik Date: Tue, 13 Oct 2026 05:14:43 +0200 Subject: [PATCH] shader: restrict shared-list descriptor bounds to overlapping producers --- .../recompiler/ir/passes/ResourceTracking.cpp | 46 +++++++++++----- tests/ResourceTrackingTests.cpp | 53 ++++++++++++++++--- 2 files changed, 79 insertions(+), 20 deletions(-) diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp index bbcd44e0d..5baadd3b5 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp @@ -1948,21 +1948,41 @@ private: } if (append.store == nullptr) return {}; } - if (std::ranges::none_of(appends, [&](const Append& at) { + const auto consumed = std::ranges::find_if(appends, [&](const Append& at) { return at.counter == consumed_counter && at.base == read_offset.offset + read_memory->offset; - })) return {}; - // Materialization validates every producer bound through this union against - // MaxIndirectDescriptorProbes. Thus counters, lane enumeration and byte addresses - // cannot wrap. Overlapping payload lists remain within the union; OOB LDS reads yield zero. - Value bound = appends.front().bound; - for (size_t index = 1; index < appends.size(); ++index) { - if (EquivalentValue(m_program, bound, appends[index].bound)) continue; - auto& maximum = m_program.value_storage.emplace_back(ValueOpcode::UMax32); - maximum.SetArg(0, bound); - maximum.SetArg(1, appends[index].bound); - bound = Value(&maximum); + }); + if (consumed == appends.end()) return {}; + const auto emit = [&](ValueOpcode op, std::initializer_list args) { + auto& node = m_program.value_storage.emplace_back(op); + size_t index = 0; + for (const auto arg: args) node.SetArg(index++, arg); + return Value(&node); + }; + Value bound = consumed->bound; + Value all_bounds = bound; + for (const auto& append: appends) { + if (EquivalentValue(m_program, consumed->bound, append.bound)) continue; + all_bounds = emit(ValueOpcode::UMax32, {all_bounds, append.bound}); + auto contribution = append.bound; + if (append.base != consumed->base) { + // Each list occupies at most its producer bound in DWORDs. Only a list + // whose possible payload span overlaps the consumed span can supply its values. + const auto gap = (std::max(append.base, consumed->base) - + std::min(append.base, consumed->base)) / 4u; + const auto overlap = emit(ValueOpcode::ULessThan32, + {Value(gap), append.base < consumed->base ? append.bound : consumed->bound}); + contribution = emit(ValueOpcode::SelectU32, {overlap, contribution, Value(0u)}); + } + bound = emit(ValueOpcode::UMax32, {bound, contribution}); } - return bound; + if (all_bounds == consumed->bound) return bound; + bound = emit(ValueOpcode::SelectU32, + {emit(ValueOpcode::ULessThan32, {Value(0u), consumed->bound}), bound, Value(0u)}); + // Validate even disjoint producers: the address and counter proofs require + // every bound to fit the existing cap, including when the consumer is empty. + return emit(ValueOpcode::SelectU32, + {emit(ValueOpcode::ULessThanEqual32, {all_bounds, Value(MaxIndirectDescriptorProbes)}), + bound, all_bounds}); } bool MatchCountedRows(const Inst& read, DescriptorSource& count_source, uint32_t& row_stride) { diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index 68df81d5f..57e56eaa0 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -2969,7 +2969,7 @@ void TestSharedAppendSelectorBounds() { OuterReentry, InnerAppendCycle, EarlyCounterRead, WrongConsumerBound, ExtraWriter, CounterAlias, PayloadWrap, CounterOob }; - const auto make_plan = [](Variant variant) { + const auto make_plan = [](Variant variant, uint32_t first_base = 1032u, uint32_t consumed_base = 8u) { Fixture fixture; ShaderComputeInputInfo compute; compute.threads_num[0] = 8u; @@ -3072,7 +3072,8 @@ void TestSharedAppendSelectorBounds() { 0, append); const auto payload = fixture.Emit(ValueOpcode::SelectU32, {active, candidate, Value(UINT32_MAX)}, 0, append); - const auto base = list == 0u ? 1032u : variant == Variant::PayloadWrap ? 0xfffffff8u : 8u; + const auto base = list == 0u ? first_base : + variant == Variant::PayloadWrap ? 0xfffffff8u : consumed_base; fixture.Emit(ValueOpcode::WriteSharedU32, {address, payload, active}, shared(base), append); if (variant == Variant::InnerAppendCycle && list == 1u) { auto *repeat = fixture.AddBlock(); @@ -3106,7 +3107,8 @@ void TestSharedAppendSelectorBounds() { choose(header, body, exit, in_list, CFG::BranchCondition::VccNonZero); fixture.block = body; const auto selected = fixture.Emit(ValueOpcode::LoadSharedU32, - {fixture.Emit(ValueOpcode::ShiftLeftLogical32, {Value(&index), Value(2u)}), Value(true)}, shared(8u)); + {fixture.Emit(ValueOpcode::ShiftLeftLogical32, {Value(&index), Value(2u)}), Value(true)}, + shared(consumed_base)); const auto offset32 = fixture.Emit(ValueOpcode::ShiftLeftLogical32, {selected, Value(5u)}); const auto offset = fixture.Emit(ValueOpcode::ReadFirstLane, {fixture.Emit(ValueOpcode::IAdd32, @@ -3181,12 +3183,26 @@ void TestSharedAppendSelectorBounds() { for (const auto counts : {std::array{1u, 2u}, std::array{3u, 1u}, std::array{1u, 1u}, std::array{0u, 0u}}) { memory.words[0x800u / 4u] = counts[0]; memory.words[0x804u / 4u] = counts[1]; - const auto expected = std::max(counts[0], counts[1]); + const auto expected = counts[1]; Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.images.size() == std::max(expected, 1u) && (expected == 0u || snapshot.images.back().dwords[0] == 0x100u + expected), - "shared append union bound or refreshed runtime counts selected stale material keys"); + "disjoint shared append lists or refreshed counts selected unrelated material keys"); } + memory.words[0x800u / 4u] = 24u; + memory.words[0x804u / 4u] = 2u; + memory.words[(144u + 320u) / 4u] = 0xbe7cc577u; + memory.fail_address = 0x1000u + 144u + 320u; + user_data[2] = 24u; + Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.images.size() == 2u && + snapshot.images[0].dwords[0] == 0x101u && snapshot.images[1].dwords[0] == 0x102u, + "a disjoint 24-entry list exposed float fields after the two active material records"); + for (const uint64_t address : {0x1800u, 0x1804u}) + Check(std::ranges::find(snapshot.specialization_reads, std::pair{address, uint64_t{4u}}) != + snapshot.specialization_reads.end(), + "a disjoint append producer escaped count capture"); + memory.fail_address = UINT64_MAX; + user_data[2] = 8u; memory.words[0x800u / 4u] = 1u; memory.words[0x804u / 4u] = 2u; memory.words[(144u + 160u) / 4u] = 4u; @@ -3207,12 +3223,15 @@ void TestSharedAppendSelectorBounds() { } } - struct BoundaryMemory { uint32_t keys_read = 0; } boundary; + struct BoundaryMemory { + uint32_t keys_read = 0; + std::array counts{0u, 65536u}; + } boundary; const auto read_boundary = +[](void *userdata, uint64_t address, std::span words) { auto &memory = *static_cast(userdata); std::ranges::fill(words, 0u); if (words.size() == 1u && (address == 0x40000000u || address == 0x40000004u)) { - words[0] = address == 0x40000000u ? 65536u : 0u; + words[0] = memory.counts[(address - 0x40000000u) / 4u]; return true; } if (words.size() == 1u && address >= 0x1090u && @@ -3238,6 +3257,26 @@ void TestSharedAppendSelectorBounds() { boundary.keys_read == 65536u && snapshot.images.size() == 1u && snapshot.images[0].dwords[0] == 0x101u, "shared append materialization rejected its exact nonwrapping probe limit"); + const std::array bases{std::pair{1032u, 8u}, std::pair{8u, 1032u}, std::pair{8u, 8u}}; + struct OverlapCase { std::array counts; std::array expected; }; + const std::array overlaps{{ + {{300u, 256u}, {256u, 300u, 300u}}, {{300u, 257u}, {300u, 300u, 300u}}, + {{300u, 256u}, {256u, 300u, 300u}}, {{256u, 3u}, {3u, 3u, 256u}}, + {{257u, 3u}, {3u, 257u, 257u}}, {{300u, 0u}, {0u, 0u, 0u}}, + {{0u, 300u}, {300u, 300u, 300u}}}}; + for (uint32_t layout = 0; layout < bases.size(); ++layout) { + auto overlap_plan = make_plan(Variant::Valid, bases[layout].first, bases[layout].second); + for (const auto& test : overlaps) { + boundary.counts = test.counts; + boundary.keys_read = 0u; + Check(MaterializeResources(overlap_plan, + {.user_data = user_data, .read_memory = read_boundary, .userdata = &boundary, + .read_specialization_memory = read_boundary}, snapshot, specialization) && + boundary.keys_read == test.expected[layout] && snapshot.images.size() == 1u && + snapshot.images[0].dwords[0] == (test.expected[layout] != 0u ? 0x101u : 0u), + "shared append bounds lost overlap, included a touching list or retained stale keys"); + } + } } void TestComputeBufferFill() {