diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index 4d3d15295..967259077 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -357,17 +357,32 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p table.OutOfBounds() != 0u || (table_base & 3u) != 0u || (table.Stride() & 3u) != 0u || table.Stride() < indirect.table_record_bytes))) return false; - // Descriptor bytes are refreshed together. The GPU selects the DWORD address; - // no material selector or unused table field is interpreted on the CPU. - keys.resize(table_size / 4u); - if (!ReadScalarTable(table_base, table_size, 0u, runtime, keys)) return false; + const bool affine = indirect.table_stride != 0u; + const uint64_t step = affine ? uint64_t {1} << std::countr_zero(indirect.table_stride) : 4u; + const uint64_t first = ((indirect.table_offset % step) & ~uint64_t {3}) + + (indirect.table_immediate & ~3u); + const auto count = table_size >= first + 4u + ? std::min((table_size - first - 4u) / step + 1u, (uint64_t {1} << 32u) / step) + : 0u; + if (affine && count > MaxIndirectDescriptorProbes) return false; + // Affine SMEM offsets wrap in U32 before adding the aligned immediate. + // Read only their congruence class; unrestricted image tables retain a bulk read. + if (!affine) { + keys.resize(table_size / 4u); + if (!ReadScalarTable(table_base, table_size, 0u, runtime, keys)) return false; + } const auto mapping_offset = snapshot.flattened_srt.size(); descriptors[resource_index] = {.dword_count = dword_count}; snapshot.flattened_srt.resize(mapping_offset + 2u, 0u); - for (uint32_t word = 0; word < keys.size(); ++word) { + for (uint32_t word = 0; word < count; ++word) { DescriptorValue candidate {.dword_count = dword_count}; - std::copy_n(keys.begin() + word, std::min(dword_count, keys.size() - word), - candidate.dwords.begin()); + if (affine) { + if (!ReadScalarTable(table_base, table_size, first + word * step, runtime, + std::span(candidate.dwords).first(dword_count))) return false; + } else { + std::copy_n(keys.begin() + word, std::min(dword_count, keys.size() - word), + candidate.dwords.begin()); + } if (!normalize(candidate)) return false; uint32_t ordinal; if (!intern_candidate(candidate, ordinal)) return false; diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp index 3ccaf8166..049ed3f9a 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp @@ -470,7 +470,16 @@ public: auto* block = anchor->Parent(); const auto where = std::ranges::find_if(block->Instructions(), [&](const Inst& inst) { return &inst == anchor; }); - auto& selection = *block->PrependNewInst(where, ValueOpcode::GetBufferResourceIndex, {plan.key}); + const auto& indirect = *m_sources[plan.source].indirect_descriptor; + if (indirect.table_scalar) { + // Wrapped affine offsets span one power-of-two congruence class. + // Keep the existing mapping's byte key while compressing its empty gaps. + key = plan.reads[0]->Arg(1); + const auto shift = std::countr_zero(indirect.table_stride) - 2u; + if (shift != 0u) key = Value(&*block->PrependNewInst( + where, ValueOpcode::ShiftRightLogical32, {key, Value(shift)})); + } + auto& selection = *block->PrependNewInst(where, ValueOpcode::GetBufferResourceIndex, {key}); selection.SetFlags(plan.handle->Flags()); selected = Value(&selection); buffer_selections.emplace_back(&plan, selected); @@ -2875,7 +2884,14 @@ private: indirect.signed_count = bound->GetOpcode() == ValueOpcode::SLessThan32; return finish(); } - if (!legacy_store || table_stride != 16u) return false; + if (!legacy_store) { + // A bounded V# supplies every reachable descriptor without reading the GPU key. + // Separate U32 offsets could wrap each DWORD to a different table position. + if (table_source.dword_count != 4u || plan.split_offsets) return false; + indirect.table_scalar = true; + return finish(); + } + if (table_stride != 16u) return false; if (table_source.dword_count == 2u) { if (!MatchPackedScalarKey(key, indirect)) { // EXEC implications must describe the same iteration as the selected value. diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index 996036b49..c1c272259 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -4393,13 +4393,26 @@ void TestBoundedFormattedBufferStore() { "finite store accepted an unproved induction or indexed byte offset"); } -void TestBoundedBufferRecords() { +std::unique_ptr MakeBufferRecordFixture(uint32_t step = 1u, bool bypass = false, + bool gpu_selected = false, uint32_t bias = 0u, + bool split_offsets = false) { namespace CFG = Libs::Graphics::ShaderRecompiler::CFG; - namespace Prospero = Libs::Graphics::Prospero; - using Libs::Graphics::DstSel; - const auto make = [](uint32_t step = 1u, bool bypass = false) { - auto fixture = std::make_unique(); - auto& f = *fixture; + auto fixture = std::make_unique(); + auto& f = *fixture; + std::array table_words; + for (uint32_t word = 0; word < 4u; ++word) table_words[word] = f.UserData(word); + const auto table = f.Buffer(table_words); + const auto lane = f.Emit(ValueOpcode::LaneId); + Value key; + if (gpu_selected) { + std::array selector_words; + for (uint32_t word = 0; word < 4u; ++word) selector_words[word] = f.UserData(4u + word); + const auto group = f.Emit(ValueOpcode::GetBuiltin, + {Value(static_cast(StageInputKind::WorkgroupId)), Value(0u)}); + key = f.Emit(ValueOpcode::ReadConstBuffer, + {f.Buffer(selector_words), f.Emit(ValueOpcode::IMul32, {group, Value(12u)})}, + f.AddMemory({.kind = ResourceKind::ScalarBuffer}, 0x1bcu)); + } else { auto* entry = f.block; auto* header = f.AddBlock(); auto* body = f.AddBlock(); @@ -4417,46 +4430,52 @@ void TestBoundedBufferRecords() { header->terminator = {.kind = CFG::TerminatorKind::ConditionalBranch, .true_block = body, .false_block = bypass ? latch : exit}; exit->terminator.kind = CFG::TerminatorKind::Return; - std::array table_words; - for (uint32_t word = 0; word < 4u; ++word) table_words[word] = f.UserData(word); - const auto table = f.Buffer(table_words); const auto count = f.Emit(ValueOpcode::LoadAddressU32, {f.Address(f.UserData(4u), f.UserData(5u)), Value(0u), Value(0u), Value(true)}, f.AddMemory({.kind = ResourceKind::ScalarAddress}, 0x424u)); - const auto lane = f.Emit(ValueOpcode::LaneId); auto& phi = header->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32)); phi.AddPhiOperand(entry, Value(0u)); phi.AddPhiOperand(latch, f.Emit(ValueOpcode::IAdd32, {Value(&phi), Value(step)}, 0, latch)); header->condition = f.Emit(ValueOpcode::SLessThan32, {Value(&phi), count}, 0, header); f.block = body; - const auto offset = f.Emit(ValueOpcode::IMul32, {Value(&phi), Value(192u)}); - for (const bool formatted : {true, false}) { - std::array words; - for (uint32_t word = 0; word < 4u; ++word) - words[word] = f.Emit(ValueOpcode::ReadConstBuffer, {table, offset}, - f.AddMemory({.kind = ResourceKind::ScalarBuffer, - .offset = (formatted ? 160u : 176u) + word * 4u, - .component_index = word, .component_count = 4u}, 0x1808u)); - for (uint32_t use = 0; use < (formatted ? 112u : 16u); ++use) { - const auto handle = f.Buffer(words); - if (formatted) { - const auto loaded = f.Emit(ValueOpcode::LoadBufferU32x2, - {handle, lane, Value(0u), Value(0u), Value(true)}, - f.AddMemory({.kind = ResourceKind::Buffer, .data_dwords = 2u, .data_bits = 16u, - .component_count = 4u, .formatted = true, .idxen = true}, 0x1914u)); - f.Emit(ValueOpcode::ReferenceU32, - {f.Emit(ValueOpcode::CompositeExtractU32x2, {loaded, Value(0u)})}); - } else { - f.Emit(ValueOpcode::StoreBufferU8, - {handle, lane, Value(0u), Value(0u), Value(uint8_t{42u}), Value(true)}, - f.AddMemory({.kind = ResourceKind::Buffer, .data_bits = 8u, .idxen = true}, 0x190cu)); - } + key = Value(&phi); + } + auto offset = f.Emit(ValueOpcode::IMul32, {key, Value(192u)}); + if (bias != 0u) offset = f.Emit(ValueOpcode::IAdd32, {offset, Value(bias)}); + for (const bool formatted : {true, false}) { + std::array words; + for (uint32_t word = 0; word < 4u; ++word) { + const auto dynamic = split_offsets + ? f.Emit(ValueOpcode::IAdd32, {offset, Value(word * 4u)}) : offset; + words[word] = f.Emit(ValueOpcode::ReadConstBuffer, {table, dynamic}, + f.AddMemory({.kind = ResourceKind::ScalarBuffer, + .offset = (formatted ? 160u : 176u) + (split_offsets ? 0u : word * 4u), + .component_index = word, .component_count = 4u}, 0x1808u)); + } + for (uint32_t use = 0; use < (gpu_selected ? 2u : formatted ? 112u : 16u); ++use) { + const auto handle = f.Buffer(words); + if (formatted) { + const auto loaded = f.Emit(ValueOpcode::LoadBufferU32x2, + {handle, lane, Value(0u), Value(0u), Value(true)}, + f.AddMemory({.kind = ResourceKind::Buffer, .data_dwords = 2u, .data_bits = 16u, + .component_count = 4u, .formatted = true, .idxen = true}, 0x1914u)); + f.Emit(ValueOpcode::ReferenceU32, + {f.Emit(ValueOpcode::CompositeExtractU32x2, {loaded, Value(0u)})}); + } else { + f.Emit(ValueOpcode::StoreBufferU8, + {handle, lane, Value(0u), Value(0u), Value(uint8_t{42u}), Value(true)}, + f.AddMemory({.kind = ResourceKind::Buffer, .data_bits = 8u, .idxen = true}, 0x190cu)); } } - f.PlanAndTrack(); - return fixture; - }; - auto fixture = make(); + } + f.PlanAndTrack(); + return fixture; +} + +void TestBoundedBufferRecords() { + namespace Prospero = Libs::Graphics::Prospero; + using Libs::Graphics::DstSel; + auto fixture = MakeBufferRecordFixture(); const auto plan = ExtractResourcePlan(fixture->program); Check(plan.info.buffers.size() == 2u, "record descriptors did not share their resource roots"); uint32_t selections = 0u; @@ -4557,9 +4576,129 @@ void TestBoundedBufferRecords() { } memory.words[0] = MaxIndirectDescriptorProbes + 1u; Check(!materialize(), "record table accepted an unbounded candidate count"); - for (const auto [step, bypass] : {std::pair{2u, false}, std::pair{1u, true}}) - CheckFatal([&] { make(step, bypass); }, "not a valid runtime value", - "record table accepted an unproved loop bound"); + for (const auto [step, bypass] : {std::pair{2u, false}, std::pair{1u, true}}) { + const auto unbounded = MakeBufferRecordFixture(step, bypass); + for (const auto& buffer : unbounded->program.info.buffers) { + const auto& source = unbounded->program.descriptor_sources[buffer.source]; + if (source.indirect_descriptor) + Check(source.indirect_descriptor->table_scalar && source.indirect_descriptor->key_count.IsEmpty(), + "unproved loop did not use the bounded descriptor table"); + } + } +} + +void TestGpuSelectedBufferRecords() { + namespace Prospero = Libs::Graphics::Prospero; + using Libs::Graphics::DstSel; + for (const uint32_t bias : {0u, 7u}) { + auto fixture = MakeBufferRecordFixture(1u, false, true, bias); + const auto plan = ExtractResourcePlan(fixture->program); + std::vector roots; + std::vector selections(plan.info.buffers.size()); + for (uint32_t resource = 0; resource < plan.info.buffers.size(); ++resource) { + const auto& source = plan.descriptor_sources[plan.info.buffers[resource].source]; + if (!source.indirect_descriptor) continue; + const auto& indirect = *source.indirect_descriptor; + Check(indirect.table_scalar && indirect.table_stride == 192u && + indirect.table_offset == bias && indirect.key_count.IsEmpty() && !indirect.selector, + "GPU record selector became a CPU key-list read or lost its affine geometry"); + roots.push_back(resource); + } + Check(roots.size() == 2u, "GPU record accesses lost their two descriptor roots"); + uint32_t selection_count = 0; + for (const auto* block : fixture->program.blocks) + for (const auto& inst : *block) + if (inst.GetOpcode() == ValueOpcode::GetBufferResourceIndex) { + selections[inst.Flags()] = &inst; + ++selection_count; + } + Check(selection_count == 2u, "GPU descriptor uses duplicated mapping searches"); + const auto evaluate_key = [&](auto&& self, Value value, uint32_t key) -> uint32_t { + value = value.Resolve(); + if (value.IsImmediate()) return value.U32(); + const auto& inst = *value.Instruction(); + if (inst.GetOpcode() == ValueOpcode::ReadConstBuffer) return key; + const auto a = self(self, inst.Arg(0), key); + const auto b = self(self, inst.Arg(1), key); + switch (inst.GetOpcode()) { + case ValueOpcode::IMul32: return a * b; + case ValueOpcode::IAdd32: return a + b; + case ValueOpcode::ShiftRightLogical32: return a >> b; + default: throw std::runtime_error("unexpected GPU descriptor key expression"); + } + }; + LinearTestMemory memory; + constexpr uint32_t table_offset = 256u; + memory.fail_address = memory.base + 0x2000u; + const auto aligned_bias = bias & ~3u; + for (uint32_t slot = 0; slot < 4u; ++slot) { + for (uint32_t member = 0; member < 2u; ++member) { + const auto first = (table_offset + aligned_bias + 160u + member * 16u + slot * 64u) / 4u; + memory.words[first] = 0x100000u + member * 0x10000u + slot * 0x1000u; + memory.words[first + 1u] = (member == 0u ? 8u : 3u) << 16u; + memory.words[first + 2u] = 16u; + memory.words[first + 3u] = member == 0u + ? (static_cast(Prospero::BufferFormat::k16_16_16_16Float) << 12u) | + DstSel(4, 5, 6, 1) : 0x5204u; + } + } + // SMEM aligns the base and dynamic offset independently; its V# format is ignored. + std::array user_data{uint32_t(memory.base) + table_offset + 3u, 0u, 0u, 0u, + uint32_t(memory.fail_address), 12u << 16u, 129600u, 0x5204u}; + SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory, + .userdata = &memory, .read_specialization_memory = ReadLinearTestMemory}; + ResourceSnapshot snapshot; + ResourceSpecialization specialization; + const auto materialize = [&] { return MaterializeResources(plan, runtime, snapshot, specialization); }; + for (const uint32_t size : {0u, 384u + aligned_bias, 0u, 384u + aligned_bias}) { + user_data[2] = size; + memory.reads = 0u; + Check(materialize() && memory.reads == (size == 0u ? 0u : 8u) && + snapshot.buffers.size() == plan.info.buffers.size() + (size == 0u ? 0u : 8u), + "affine table refresh read GPU selector bytes, unrelated fields or stale candidates"); + } + for (uint32_t member = 0; member < 2u; ++member) { + const auto root = roots[member]; + const auto mapping = specialization.buffers[root].indirect_mapping_offset; + Check(specialization.buffers[root].indirect_search_iterations == 0u && + snapshot.flattened_srt[mapping] == 4u, + "affine descriptor table lost its compact dense mapping"); + // These keys land at byte offsets 0,64,128,192 after U32 multiplication by192. + for (const auto [key, slot] : {std::pair{0u, 0u}, std::pair{44739243u, 1u}, + std::pair{22369622u, 2u}, std::pair{1u, 3u}}) { + const auto index = evaluate_key(evaluate_key, selections[root]->Arg(0), key) >> 2u; + Check(index == slot && snapshot.flattened_srt[mapping + 1u + index] == slot + 1u, + "wrapped GPU record key selected the wrong physical descriptor"); + const auto candidate = plan.info.buffers.size() + member * 4u + slot; + Check(snapshot.buffers[candidate].dwords[0] == 0x100000u + member * 0x10000u + slot * 0x1000u, + "affine table read a stride192 row instead of a reachable stride64 member"); + Check(std::ranges::find(snapshot.specialization_reads, + std::pair{memory.base + table_offset + aligned_bias + 160u + member * 16u + slot * 64u, + uint64_t{16u}}) != snapshot.specialization_reads.end(), + "affine descriptor read escaped writable-alias capture"); + } + } + // The trailing V# has only three readable DWORDs; its missing format word is zero. + const auto tail = (table_offset + aligned_bias + 416u) / 4u; + memory.words[tail] = 0x300000u; + memory.words[tail + 1u] = 8u << 16u; + memory.words[tail + 2u] = 16u; + memory.words[tail + 3u] = 0x5204u; + user_data[2] = 428u + aligned_bias; + memory.reads = 0u; + Check(materialize() && memory.reads == 9u && + snapshot.buffers.size() == plan.info.buffers.size() + 8u && + std::ranges::find(snapshot.specialization_reads, + std::pair{memory.base + table_offset + aligned_bias + 416u, uint64_t{12u}}) != + snapshot.specialization_reads.end(), + "partial descriptor did not zero-fill and capture only in-bounds DWORDs"); + user_data[2] = aligned_bias + 160u + MaxIndirectDescriptorProbes * 64u + 4u; + memory.reads = 0u; + Check(!materialize() && memory.reads == 0u, + "oversized affine table was read before its candidate cap was checked"); + } + CheckFatal([] { MakeBufferRecordFixture(1u, false, true, 0u, true); }, + "not a valid runtime value", "split dynamic descriptor offsets lost independent U32 wrapping"); } void TestFiniteBufferSelection() { @@ -6058,6 +6197,7 @@ int main() { Run("writable descriptor phi", TestWritableDescriptorPhi); Run("bounded formatted buffer store", TestBoundedFormattedBufferStore); Run("bounded buffer records", TestBoundedBufferRecords); + Run("GPU-selected buffer records", TestGpuSelectedBufferRecords); Run("finite buffer selection", TestFiniteBufferSelection); Run("packed scalar buffer selection", TestPackedScalarBufferSelection); Run("conditional sampler phi", TestConditionalSamplerPhi); diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 530d50a61..086d2196e 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -36766,6 +36766,105 @@ void CheckBoundedRecordBufferAccess(VulkanHarness &vulkan) { std::printf("[compute] %-32s ok\n", "BoundedRecordBufferAccess"); } +void CheckGpuSelectedRecordBufferAccess(VulkanHarness &vulkan) { + const u32 alignment = vulkan.RuntimeContext().GetPhysicalDeviceProperties() + .limits.minStorageBufferOffsetAlignment; + const u32 slot = (4096u + alignment - 1u) / alignment * alignment; + constexpr u32 selectors = 256u, table = 512u; + // Multiplication by 192 wraps: these keys reach congruence slots 1,4,0,3,2,1,OOB,7,5. + constexpr std::array keys{0u, 1u, 0x15555555u, 0x15555556u, 0x2aaaaaabu, + 0x40000000u, UINT32_MAX, 2u, 0x2aaaaaacu}; + for (const u32 wave : {32u, 64u}) { + for (const u32 table_bytes : {0u, 536u, 544u}) { + TestCase test; + test.name = wave == 32u ? "GpuSelectedRecordBufferAccess32" : "GpuSelectedRecordBufferAccess64"; + test.initial.assign((slot * 13u + keys.size() * wave * 8u) / 4u, 0xdeadbeefu); + test.has_user_data = test.has_compute_info = true; + const std::array descriptors{selectors, 12u << 16u, keys.size(), 0x5204u, + table, 0u, table_bytes, 0x5204u}; + std::copy(descriptors.begin(), descriptors.end(), test.user_data.begin()); + const std::array output{slot * 13u, 8u << 16u, u32(keys.size()) * wave, 0x5204u}; + std::copy(output.begin(), output.end(), test.user_data.begin() + 16u); + test.compute_info.wave_size = wave; + test.compute_info.threads_num[0] = wave; + test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1u; + test.compute_info.thread_ids_num = 1; + test.compute_info.group_id[0] = true; + test.compute_info.workgroup_register = 24u; + test.compute_info.lds_size_dwords = 0; + test.dispatch_x = keys.size(); + auto &code = test.code; + code.insert(code.end(), {EncodeSop2(0x26, 32, 24, InlineU32(12)), + EncodeSmem0(0x08, 32, 0), EncodeSmem1(0u, 32), EncodeSopp(0x0c, 0), + EncodeSop2(0x26, 33, 32, 255), 192u, + EncodeSop2(0x02, 33, 33, 255), 96u, + EncodeSmem0(0x0a, 8, 2), EncodeSmem1(160u, 33), + EncodeSmem0(0x0a, 12, 2), EncodeSmem1(176u, 33), EncodeSopp(0x0c, 0), + EncodeMubuf0(0x83, 0, true, false), EncodeMubuf1(4, 2, 0), + EncodeSopp(0x0c, 0), EncodeSop2(0x1e, 34, 24, InlineU32(std::countr_zero(wave))), + EncodeVop2(0x25, 20, 34, 0), + EncodeMubuf0(0x1d, 0, true, false), EncodeMubuf1(4, 4, 20)}); + AppendVMovLiteral(&code, 10, 0xa5c39b72u); + AppendSMovLiteral(&code, 126, 0x55555555u); + AppendSMovLiteral(&code, 127, wave == 64u ? 0x55555555u : 0u); + code.insert(code.end(), {EncodeMubuf0(0x19, 0, true, false, true), EncodeMubuf1(10, 3, 20)}); + AppendEnd(&code); + for (u32 row = 0; row < 6u; ++row) { + const std::array record{slot * (row + 1u), 8u << 16u, 1u, + static_cast(Prospero::BufferFormat::k16_16_16_16Float) << 12u | DstSel(4, 5, 6, 7), + slot * (row + 7u), 1u << 16u, u32(keys.size()) * wave, 0x5204u}; + std::copy(record.begin(), record.end(), test.initial.begin() + (table + 192u + row * 64u) / 4u); + test.initial[slot * (row + 1u) / 4u] = ((0x3800u + row) << 16u) | (0x3c00u + row); + test.initial[slot * (row + 1u) / 4u + 1u] = ((0xc000u + row) << 16u) | (0x4000u + row); + } + // Compile against poison selectors. A separate GPU dispatch writes the actual keys. + auto compiled = CompileCase(test, vulkan.SubgroupSize()); + for (const auto &descriptor : compiled.resources.buffers) { + ShaderBufferResource resource{}; + std::copy_n(descriptor.dwords.begin(), 4u, resource.fields); + test.storage_buffer_ranges.emplace_back(resource.Base48(), std::max(4u, u32(resource.GetSize()))); + } + TestCase producer; + producer.name = "GpuRecordSelectorProducer"; + producer.initial = test.initial; + producer.has_compute_info = true; + producer.compute_info.threads_num[0] = producer.compute_info.threads_num[1] = + producer.compute_info.threads_num[2] = 1u; + producer.compute_info.wave_size = 32u; + producer.compute_info.lds_size_dwords = 0; + for (u32 group = 0; group < keys.size(); ++group) { + AppendVMovLiteral(&producer.code, 1, keys[group]); + AppendVMovLiteral(&producer.code, 2, selectors + group * 12u); + AppendBufferStoreDword(&producer.code, 1, 2); + } + AppendEnd(&producer.code); + const auto producer_shader = CompileCase(producer, vulkan.SubgroupSize()); + test.expected = test.initial; + auto bytes = std::as_writable_bytes(std::span(test.expected)); + for (u32 group = 0; group < keys.size(); ++group) { + test.expected[selectors / 4u + group * 3u] = keys[group]; + const u32 dynamic = keys[group] * 192u + 96u; + const u32 row = dynamic / 64u; + for (u32 lane = 0; lane < wave; ++lane) { + const u32 out = slot * 13u / 4u + (group * wave + lane) * 2u; + const bool source = uint64_t{dynamic} + 160u + 16u <= table_bytes && lane == 0u; + test.expected[out] = source ? test.initial[slot * (row + 1u) / 4u] : 0u; + test.expected[out + 1u] = source ? test.initial[slot * (row + 1u) / 4u + 1u] : 0u; + if (uint64_t{dynamic} + 176u + 16u <= table_bytes && (lane & 1u) == 0u) + bytes[slot * (row + 7u) + group * wave + lane] = std::byte{0xc3}; + } + } + auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size()); + vulkan.Dispatch(producer, producer_shader, buffer); + vulkan.Dispatch(test, compiled, buffer); + CompareWords(test, "GPU selectors, wrapped offsets, partial descriptors, OOB and masked byte stores", + test.expected, vulkan.ReadBuffer(test.name, buffer, test.expected.size())); + vulkan.DestroyBuffer(&buffer); + } + } + std::printf("[compute] %-32s ok\n", "GpuSelectedRecordBufferAccess"); +} + void CheckFiniteBufferSelection(VulkanHarness &vulkan) { for (const u32 wave : {32u, 64u}) { TestCase test; @@ -45574,6 +45673,7 @@ int main(int argc, char **argv) { CheckFiniteBufferSelection(vulkan); CheckIndirectBufferStore(vulkan); CheckBoundedRecordBufferAccess(vulkan); + CheckGpuSelectedRecordBufferAccess(vulkan); CheckRuntimeBufferRecords(vulkan); RunCase(&vulkan, BufferLoadsGpuSelectedDescriptors()); RunCase(&vulkan, BufferLoadDwordGpuSelectedDescriptors()); @@ -46278,6 +46378,7 @@ int main(int argc, char **argv) { CheckBoundedFormattedBufferCopy(vulkan); CheckIndirectBufferStore(vulkan); CheckBoundedRecordBufferAccess(vulkan); + CheckGpuSelectedRecordBufferAccess(vulkan); CheckFiniteBufferSelection(vulkan); CheckRuntimeBufferRecords(vulkan); CheckComputeThreadDimensions(vulkan);