From 2caf2f7e4e53828e0c48e1e8a51c24ddd31da6e6 Mon Sep 17 00:00:00 2001 From: nmzik Date: Thu, 15 Oct 2026 16:37:56 +0200 Subject: [PATCH] shader: retain runtime count bounds for finite descriptor choices --- .../ir/passes/ResourceMaterialization.cpp | 15 ++- .../recompiler/ir/passes/ResourceTracking.cpp | 68 ++++++++--- tests/ResourceTrackingTests.cpp | 113 ++++++++++++++++-- tests/ShaderRecompilerComputeTests.cpp | 34 +++++- 4 files changed, 192 insertions(+), 38 deletions(-) diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index 320d54b90..e193c72b2 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -292,7 +292,7 @@ bool IsBoundedDescriptorTable(const ResourcePlan& program, const DescriptorSource::IndirectDescriptor& indirect) { const auto* table = Source(program, indirect.table_source); return indirect.sources.empty() && !indirect.selector && - indirect.key_count.IsEmpty() && indirect.workgroup_axis == UINT32_MAX && + (indirect.key_count.IsEmpty() || indirect.table_maximum) && indirect.workgroup_axis == UINT32_MAX && table != nullptr && table->dword_count == 4u; } @@ -609,11 +609,19 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p const auto* pointer = indirect.pointer_table ? &*indirect.pointer_table : nullptr; const bool indexed_pointer = pointer != nullptr && pointer->indexed; const auto child_count = pointer != nullptr ? pointer->count : 1u; + uint64_t domain = indirect.table_maximum ? uint64_t {*indirect.table_maximum} + 1u + : uint64_t {1} << 32u; + if (!indirect.key_count.IsEmpty()) { + uint32_t key_count = 0u; + if (!EvaluateIndirectCount(indirect, runtime, clean, key_count)) return false; + domain = std::min(domain, uint64_t {key_count}); + } if (indexed_pointer && (!indirect.table_maximum || table.Type() != 0u || table.SwizzleEnabled() || table.AddTid() || table.OutOfBounds() != 0u || (table_base & 3u) != 0u || (table.Stride() & 3u) != 0u || uint64_t {indirect.table_immediate} + 8u > table.Stride() || - *indirect.table_maximum >= table.NumRecords())) return failed("indexed pointer geometry"); + domain > table.NumRecords())) + return failed("indexed pointer geometry", fmt::format("rows={}", domain)); const bool affine = indirect.table_stride != 0u || indexed_pointer; const bool image_heap = dword_count == 8u && pointer == nullptr; const uint64_t step = indexed_pointer ? table.Stride() @@ -623,8 +631,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p const uint64_t first = ((indirect.table_maximum ? indirect.table_offset : indirect.table_offset % step) & ~uint64_t {3}) + (indirect.table_immediate & ~3u); - const uint64_t domain = indirect.table_maximum ? uint64_t {*indirect.table_maximum} + 1u - : (uint64_t {1} << 32u) / step; + if (!indirect.table_maximum) domain /= step; const auto count = table_size >= first + 4u ? std::min((table_size - first - 4u) / step + 1u, domain) : 0u; diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp index e7f79b239..26455095c 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp @@ -2788,19 +2788,26 @@ private: bool SetExactTableMaximum(Value key, DescriptorSource::IndirectDescriptor& indirect, const Inst* handle = nullptr) { - uint32_t maximum = 0u; + const auto* lane = key.Resolve().TryInstruction(); + if (lane != nullptr && lane->GetOpcode() != ValueOpcode::ReadFirstLane) lane = nullptr; + SelectorDomain result {0u}; if (handle != nullptr) { if (handle->Uses().empty()) return false; for (const auto& use: handle->Uses()) { if (ImageOpcodeInfoOf(use.user->GetOpcode()).access == ImageAccess::None) return false; - maximum = std::max(maximum, SelectorMaximum(key, {}, 0u, - use.user->Arg(use.user->NumArgs() - 1u))); + const auto active = use.user->Arg(use.user->NumArgs() - 1u); + if (lane != nullptr) { + if (!Implies(active, lane->Arg(1))) return false; + } else result.maximum = std::max(result.maximum, SelectorMaximum(key, {}, 0u, active)); } - } else maximum = SelectorMaximum(key); - if (maximum > UINT32_MAX / 4u || - uint64_t {maximum} * indirect.table_stride + indirect.table_offset > UINT32_MAX) + } + if (lane != nullptr) result = SelectedFiniteDomain(*lane, handle != nullptr ? lane->Arg(1) : Value {}); + else if (handle == nullptr) result.maximum = SelectorMaximum(key); + if (result.maximum > UINT32_MAX / 4u || + uint64_t {result.maximum} * indirect.table_stride + indirect.table_offset > UINT32_MAX) return false; - indirect.table_maximum = maximum; + indirect.table_maximum = result.maximum; + indirect.key_count = result.count; return true; } @@ -3272,7 +3279,12 @@ private: return false; } - uint32_t FiniteSelectorMaximum(Value value, Value guard) const { + struct SelectorDomain { + uint32_t maximum = UINT32_MAX; + Value count; + }; + + SelectorDomain FiniteSelectorDomain(Value value, Value guard) const { value = value.Resolve(); bool nonnegative = false; if (const auto* root = value.TryInstruction()) { @@ -3286,31 +3298,55 @@ private: Implies(guard, Value(use.user))) nonnegative = true; } } - std::vector pending {value}; + struct Arm { Value value; const Inst* choice = nullptr; bool positive = true; }; + std::vector pending {{value}}; std::vector visited; uint32_t maximum = 0u; bool found = false; + bool counted = true; + Value count; for (size_t at = 0; at < pending.size(); ++at) { uint32_t literal; - const auto current = pending[at].Resolve(); + const auto current = ActiveValue(pending[at].value, guard); if (ImmediateU32(current, literal)) { if (nonnegative && std::bit_cast(literal) < 0) continue; maximum = std::max(maximum, literal); found = true; + if (counted) { + Value candidate; + if (const auto* choice = pending[at].choice; choice != nullptr) { + if (choice->GetOpcode() == ValueOpcode::SelectU32) + candidate = UnsignedBound(choice->Arg(0), current, pending[at].positive, + false, pending[at].positive ? choice->Arg(0) : Value {}); + if (candidate.IsEmpty() && choice->Parent() != nullptr) + candidate = UnsignedBoundOnEntry(current, choice->Parent()); + } + if (candidate.IsEmpty() || !ValidateRuntimeValue(m_program, candidate, RuntimeValueType::Integer) || + (!count.IsEmpty() && !EquivalentValue(m_program, count, candidate))) counted = false; + else count = candidate; + } continue; } const auto* choice = current.TryInstruction(); - if (choice == nullptr) return UINT32_MAX; + if (choice == nullptr) return {}; const bool phi = choice->GetOpcode() == ValueOpcode::Phi; if ((!phi && choice->GetOpcode() != ValueOpcode::SelectU32) || (phi && (choice->NumArgs() == 0u || choice->Parent() == nullptr || - InCycle(choice->Parent())))) return UINT32_MAX; + InCycle(choice->Parent())))) return {}; + // Each literal uses only its own choice and block-entry proof. A shared + // choice therefore cannot borrow a bound from a different parent path. if (std::ranges::find(visited, choice) != visited.end()) continue; visited.push_back(choice); for (size_t arm = phi ? 0u : 1u; arm < choice->NumArgs(); ++arm) - pending.push_back(choice->Arg(arm)); + pending.push_back({choice->Arg(arm), choice, arm == 1u}); } - return found ? maximum : UINT32_MAX; + return found ? SelectorDomain {maximum, counted ? count : Value {}} : SelectorDomain {}; + } + + SelectorDomain SelectedFiniteDomain(const Inst& lane, Value guard) const { + if (guard.IsEmpty() ? !HasActiveLane(lane.Arg(1), lane.Parent()) + : !Implies(guard, lane.Arg(1))) return {}; + return FiniteSelectorDomain(lane.Arg(0), lane.Arg(1)); } uint32_t SelectorMaximum(Value value, SelectorBounds bounds = {}, uint32_t depth = 0, @@ -3327,9 +3363,7 @@ private: }; switch (inst->GetOpcode()) { case ValueOpcode::ReadFirstLane: - if (guard.IsEmpty() ? !HasActiveLane(inst->Arg(1), inst->Parent()) - : !Implies(guard, inst->Arg(1))) return UINT32_MAX; - return FiniteSelectorMaximum(inst->Arg(0), inst->Arg(1)); + return SelectedFiniteDomain(*inst, guard).maximum; case ValueOpcode::ReadLane: // ReadLane ignores EXEC; only context-free bounds on every source lane apply. return PossibleU32Bits(inst->Arg(0)); diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index 3ad64f396..2d3eed9ba 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -986,19 +986,35 @@ void TestIndexedPointerImageTable() { enum class Case { Valid, ReverseSigned, MissingParent, WrongParent, MissingChild, InclusiveChild, MismatchedPair, DifferentLaneMask, ExtraUse, UnboundedChoice, MissingOuterWitness, WrongOuterWitness, CyclicChoice, - ExcessiveDomain }; + ExcessiveDomain, Counted, MissingCount, DifferentCount, MaskedCount, UnprovenMask }; const auto make = [](Case variant = Case::Valid) { auto fixture = std::make_unique(); std::array table_words; for (uint32_t word = 0; word < 4u; ++word) table_words[word] = fixture->UserData(word); const auto table = fixture->Buffer(table_words); const auto lane = fixture->Emit(ValueOpcode::LaneId); - const auto choice = fixture->Emit(ValueOpcode::ULessThan32, {lane, Value(8u)}); - const auto valid = fixture->Emit(ValueOpcode::ULessThan32, {lane, Value(16u)}); - auto row = fixture->Emit(ValueOpcode::SelectU32, - {valid, fixture->Emit(ValueOpcode::SelectU32, - {choice, Value(0u), variant == Case::UnboundedChoice ? lane : Value(1u)}), - Value(UINT32_MAX)}); + const bool counted = variant >= Case::Counted; + Value choice, row, count, other_count; + if (counted) { + const auto address = fixture->Address(fixture->UserData(4), fixture->UserData(5)); + MemoryInfo memory; + memory.kind = ResourceKind::ScalarAddress; + count = fixture->Emit(ValueOpcode::LoadAddressU32, + {address, Value(0u), Value(0u), Value(true)}, fixture->AddMemory(memory, 0x8u)); + if (variant == Case::DifferentCount) { + memory.offset = 4u; + other_count = fixture->Emit(ValueOpcode::LoadAddressU32, + {address, Value(0u), Value(0u), Value(true)}, fixture->AddMemory(memory, 0x10u)); + } + choice = fixture->Emit(ValueOpcode::ULessThan32, {Value(0u), count}); + } else { + choice = fixture->Emit(ValueOpcode::ULessThan32, {lane, Value(8u)}); + const auto valid = fixture->Emit(ValueOpcode::ULessThan32, {lane, Value(16u)}); + row = fixture->Emit(ValueOpcode::SelectU32, + {valid, fixture->Emit(ValueOpcode::SelectU32, + {choice, Value(0u), variant == Case::UnboundedChoice ? lane : Value(1u)}), + Value(UINT32_MAX)}); + } auto* entry = fixture->block; auto* left = fixture->AddBlock(); auto* right = fixture->AddBlock(); @@ -1012,22 +1028,44 @@ void TestIndexedPointerImageTable() { from->AddBranch(merge); from->terminator = {.kind = CFG::TerminatorKind::Branch, .true_block = merge}; } + if (counted) { + fixture->block = left; + const auto selector = fixture->Emit(ValueOpcode::BitwiseAnd32, {lane, Value(3u)}); + row = Value(UINT32_MAX); + for (uint32_t key = 4u; key-- > 0u;) { + auto active = fixture->Emit(ValueOpcode::IEqual32, {selector, Value(key)}); + // Row zero uses the uniform CFG-entry bound, like the captured shader. + if (key != 0u && !(variant == Case::MissingCount && key == 2u)) { + const auto bound = variant == Case::DifferentCount && key == 2u ? other_count : count; + active = fixture->Emit(ValueOpcode::LogicalAnd, + {active, fixture->Emit(ValueOpcode::ULessThan32, {Value(key), bound})}); + } + row = fixture->Emit(ValueOpcode::SelectU32, {active, Value(key), row}); + } + } fixture->block = merge; auto& phi = merge->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32)); phi.AddPhiOperand(left, row); - phi.AddPhiOperand(right, row); + phi.AddPhiOperand(right, counted ? Value(UINT32_MAX) : row); row = Value(&phi); if (variant == Case::CyclicChoice) { merge->AddBranch(merge); merge->terminator = {.kind = CFG::TerminatorKind::Branch, .true_block = merge}; phi.AddPhiOperand(merge, row); } else merge->terminator.kind = CFG::TerminatorKind::Return; - const auto parent = variant == Case::MissingParent ? Value(true) + Value selected_mask; + if (variant == Case::MaskedCount || variant == Case::UnprovenMask) { + selected_mask = fixture->Emit(ValueOpcode::ULessThan32, {lane, Value(2u)}); + row = fixture->Emit(ValueOpcode::SelectU32, {selected_mask, row, Value(0u)}); + } + auto parent = variant == Case::MissingParent ? Value(true) : variant == Case::ReverseSigned ? fixture->Emit(ValueOpcode::SGreaterThanEqual32, {row, Value(0u)}) : fixture->Emit(variant == Case::WrongParent ? ValueOpcode::ULessThanEqual32 : ValueOpcode::SLessThanEqual32, {Value(0u), row}); + if (variant == Case::MaskedCount) + parent = fixture->Emit(ValueOpcode::LogicalAnd, {selected_mask, parent}); MemoryInfo pointer_memory; pointer_memory.kind = ResourceKind::Buffer; pointer_memory.idxen = true; @@ -1096,7 +1134,7 @@ void TestIndexedPointerImageTable() { constexpr uint32_t table = 0x2000u, objects = 0x10000u, row_bytes = 1536u; LinearTestMemory memory; memory.base += high; - memory.words.resize((objects + 2u * row_bytes - 0x1000u) / 4u, 0xdeadbeefu); + memory.words.resize((objects + 4u * row_bytes - 0x1000u) / 4u, 0xdeadbeefu); const auto at = [&](uint32_t address) { return memory.words.begin() + (address - 0x1000u) / 4u; }; const auto image_words = [](uint32_t id) { return std::array{id, @@ -1105,7 +1143,7 @@ void TestIndexedPointerImageTable() { }; // Runtime stride differs from the native sampler's 136-byte table arithmetic. constexpr uint32_t stride = 160u; - for (uint32_t row = 0; row < 2u; ++row) { + for (uint32_t row = 0; row < 4u; ++row) { *at(table + row * stride) = objects + row * row_bytes; *at(table + row * stride + 4u) = 0xbeef0001u; for (uint32_t child = 0; child < 3u; ++child) { @@ -1197,6 +1235,59 @@ void TestIndexedPointerImageTable() { CheckFatal([&] { invalid->PlanAndTrack(); }, "not a valid runtime value", "nested table accepted an unproved domain, mismatched pointer pair or unguarded use"); } + + auto counted = make(Case::Counted); + counted->PlanAndTrack(); + const auto counted_plan = ExtractResourcePlan(counted->program); + constexpr uint32_t count_address = 0x1800u; + std::array counted_data{table, 1u | (stride << 16u), 2u, 0x5204u, count_address, 1u}; + const SrtRuntime counted_runtime{.user_data = counted_data, .read_memory = ReadLinearTestMemory, .userdata = &memory, + .read_specialization_memory = ReadLinearTestMemory}; + *at(table) = objects; + *at(table + 4u) = 0xbeef0001u; + *at(count_address) = *at(count_address + 4u) = 2u; + Check(MaterializeResources(counted_plan, counted_runtime, snapshot, specialization) && snapshot.images.size() == 7u, + "finite parent choices lost their runtime count and enumerated unavailable rows"); + const auto& counted_indirect = *counted_plan.descriptor_sources[counted_plan.info.images[0].source].indirect_descriptor; + Check(counted_indirect.table_maximum == 3u && !counted_indirect.key_count.IsEmpty() && + captured(high + count_address, 4u), + "finite parent domain lost its static maximum or captured runtime count"); + auto masked = make(Case::MaskedCount); + masked->PlanAndTrack(); + const auto masked_plan = ExtractResourcePlan(masked->program); + Check(!masked_plan.descriptor_sources[masked_plan.info.images[0].source].indirect_descriptor->key_count.IsEmpty() && + MaterializeResources(masked_plan, counted_runtime, snapshot, specialization) && snapshot.images.size() == 7u, + "inactive preserved selector value discarded the selected lane's runtime count"); + memory.watched_address = high + table; + for (const uint32_t count : {0u, 1u, 2u, 4u, 5u}) { + *at(count_address) = count; + counted_data[2] = std::min(count, 4u); + memory.watched_reads = 0u; + Check(MaterializeResources(counted_plan, counted_runtime, snapshot, specialization) && + snapshot.images.size() == 1u + 3u * std::min(count, 4u) && + (count != 0u || memory.watched_reads == 0u), + "finite parent count refresh lost candidates, ignored its static cap or read an empty domain"); + } + counted_data[2] = 2u; + *at(count_address) = 3u; + memory.watched_reads = 0u; + Check(!MaterializeResources(counted_plan, counted_runtime, snapshot, specialization) && memory.watched_reads == 0u, + "runtime parent count exceeding V# records was clamped or read candidates"); + *at(count_address) = 2u; + memory.fail_address = high + count_address; + Check(!MaterializeResources(counted_plan, counted_runtime, snapshot, specialization) && memory.watched_reads == 0u, + "unreadable parent count was ignored before candidate reads"); + memory.fail_address = UINT64_MAX; + for (const auto variant : {Case::MissingCount, Case::DifferentCount, Case::UnprovenMask}) { + auto unproved = make(variant); + unproved->PlanAndTrack(); + const auto unproved_plan = ExtractResourcePlan(unproved->program); + const auto& source = unproved_plan.descriptor_sources[unproved_plan.info.images[0].source]; + memory.watched_reads = 0u; + Check(source.indirect_descriptor->key_count.IsEmpty() && + !MaterializeResources(unproved_plan, counted_runtime, snapshot, specialization) && memory.watched_reads == 0u, + "finite choice borrowed a missing or different leaf's count or pruned an unproven inactive arm"); + } } void TestGuardedDirectImageTable() { diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 2e0a936f0..8fa132448 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -39014,19 +39014,22 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) { constexpr u32 list_address = 160, objects = 4096; constexpr std::array keys{5u, 0u, 3u, 1u, 4u, 2u}; constexpr std::array list{2u, 1u, 4u, 0u, 5u, 3u}; - for (const u32 member : {0u, 0x98u, 0x16cu, 64u}) + for (const auto [member, counted] : {std::pair{0u, false}, std::pair{0x98u, false}, + std::pair{0x16cu, false}, std::pair{64u, false}, std::pair{64u, true}}) for (const auto [wave, upper_only] : {std::pair{32u, false}, std::pair{64u, false}, std::pair{64u, true}}) { const bool child_table = member == 64u, compact = member != 0u && !child_table; if (upper_only && !child_table) continue; const u32 keys_address = child_table ? 1024u : 128u, table = child_table ? 2048u : 256u; const u32 rows = child_table ? 4u : keys.size(), children = child_table ? 3u : 1u; + const u32 records_count = counted ? 2u : rows, count_address = 768u; const u32 stride = child_table ? 136u : compact ? 0x330u : 592u; const u32 object_stride = child_table ? 1536u : 0x430u; const u32 table_bytes = (rows - 1u) * stride + member + 16u; const u32 outputs = child_table ? wave * 2u : keys.size(); TestCase test; - test.name = child_table ? (upper_only ? "IndexedPointerChildTableUpper64" : "IndexedPointerChildTable") + test.name = counted ? (upper_only ? "CountedPointerChildTableUpper64" : "CountedPointerChildTable") + : child_table ? (upper_only ? "IndexedPointerChildTableUpper64" : "IndexedPointerChildTable") : member == 0x16cu ? "CompactPairedImageSamplerTable" : compact ? "CompactRecordImageSamplerTable" : "NestedPointerImageSamplerTable"; test.initial.resize((compact ? table + table_bytes : objects + rows * object_stride) / 4u, @@ -39034,7 +39037,7 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) { std::fill_n(test.initial.begin(), outputs, 0xdeadbeefu); std::fill_n(test.initial.begin() + keys_address / 4u, child_table ? wave * 2u : keys.size(), UINT32_MAX); if (compact) std::copy(list.begin(), list.end(), test.initial.begin() + list_address / 4u); - for (u32 row = 0; row < rows; ++row) { + for (u32 row = 0; row < records_count; ++row) { const u32 object = compact ? table + row * stride : objects + row * object_stride; if (!compact) { test.initial[(table + row * stride) / 4u] = object; @@ -39062,7 +39065,7 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) { test.initial.begin() + (table + (member == 0x16cu ? 0x17cu : 0xbcu)) / 4u); } auto records = MakeStructuredStorageBufferData(compact ? 0u : stride, - compact ? table_bytes : rows); + compact ? table_bytes : records_count); records[0] = table; std::copy_n(records.begin(), 4, test.user_data.begin() + 32); auto key_buffer = MakeStructuredStorageBufferData(child_table ? 8u : 0u, @@ -39076,6 +39079,10 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) { } const auto output = MakeStructuredStorageBufferData(0, outputs * 4u); std::copy_n(output.begin(), 4, test.user_data.begin() + 48); + if (counted) { + test.user_data[52] = count_address; + test.initial[count_address / 4u] = records_count; + } test.bda_mappings = {{0u, 0u}}; test.has_user_data = test.has_compute_info = true; test.compute_info.wave_size = wave; @@ -39089,6 +39096,9 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) { size_t loop = 0, empty = 0, skip_child = 0; if (child_table) { code.push_back(EncodeVop2(0x1a, 30, InlineU32(2), 0)); + if (counted) + code.insert(code.end(), {EncodeSmem0(0x00, 54, 26), EncodeSmem1(0, 125), + EncodeSopp(0x0c, 0)}); if (upper_only) { AppendSMovLiteral(&code, 126, 0u); AppendSMovLiteral(&code, 127, UINT32_MAX); @@ -39097,7 +39107,11 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) { EncodeMubuf0(0x0d, 0, true, false), EncodeMubuf1(4, 1, 0), EncodeSopp(0x0c, 0)}); AppendVMovLiteral(&code, 6, UINT32_MAX); for (u32 row = 0; row < rows; ++row) { + if (counted) + code.insert(code.end(), {EncodeSopc(0x08, 54, InlineU32(row)), + EncodeSop2(0x0b, 62, 126, InlineU32(0))}); code.push_back(EncodeVopc(0xc2, InlineU32(row), 4)); + if (counted) code.push_back(EncodeSop2(0x0f, 106, 106, 62)); AppendVop3(&code, 0x101, 6, Vgpr(6), InlineU32(row), 106); } code.push_back(EncodeVopc(0x93, InlineU32(0), 6)); // Only finite nonnegative parent rows. @@ -39156,6 +39170,8 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) { test.decoded_counts = {{"BUFFER_LOAD_DWORDX2", 2u}, {"V_READFIRSTLANE_B32", 4u}, {"V_BFE_I32", 1u}, {"V_CMPX_LE_I32", 1u}, {"S_CBRANCH_EXECNZ", 1u}}; test.required_spirv = {"OpLoopMerge", "OpGroupNonUniformShuffle", "OpBitFieldSExtract"}; + if (counted) test.decoded_counts.insert(test.decoded_counts.end(), + {{"S_LOAD_DWORD ", 1u}, {"S_CMP_GT_U32", rows}, {"S_CSELECT_B64", rows}}); } else test.opcodes = {O::S_LSHL_B32, O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_LSHRREV_B32, O::V_READLANE_B32, O::S_MUL_I32, O::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; @@ -39171,7 +39187,7 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) { for (const auto &image : compiled.resources.images) if (image.dwords[0] != 0u) candidates.push_back(image.dwords[0]); std::ranges::sort(candidates); - std::vector expected_candidates(rows * children); + std::vector expected_candidates(records_count * children); std::iota(expected_candidates.begin(), expected_candidates.end(), 10u); Require(test.name, "complete child domain", candidates == expected_candidates, "parent/child planning omitted a record or admitted poisoned padding"); @@ -39179,6 +39195,12 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) { std::ranges::none_of(compiled.resources.specialization_reads, [&](const auto &span) { return span.first < keys_address + wave * 8u && keys_address < span.first + span.second; }), "host planning captured GPU-only selector bytes"); + if (counted) + Require(test.name, "runtime parent count is captured", + std::ranges::any_of(compiled.resources.specialization_reads, [&](const auto &span) { + return span.first <= count_address && count_address - span.first < span.second && + sizeof(u32) <= span.second - (count_address - span.first); + }), "the proven scalar count was absent from the captured alias ranges"); } if (compact) { std::vector candidates; @@ -39235,7 +39257,7 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) { test.expected[address / 4u + 1u] = child; if (upper_only && group < 32u) continue; test.expected[wave + group] = 0x76543210u; - if (outer >= rows || child >= children) continue; + if (outer >= records_count || child >= children) continue; } const u32 row = compact ? list[outer] : outer; test.expected[group] = std::bit_cast(float(10u + row * children + child) *