diff --git a/src/graphics/shader/recompiler/ir/ShaderIR.h b/src/graphics/shader/recompiler/ir/ShaderIR.h index fa8535f8..4826eac3 100644 --- a/src/graphics/shader/recompiler/ir/ShaderIR.h +++ b/src/graphics/shader/recompiler/ir/ShaderIR.h @@ -459,11 +459,13 @@ struct BlockInfo { struct DescriptorSource { struct IndirectImage { - uint32_t material_source = 0; - uint32_t heap_source = 0; + uint32_t material_source = UINT32_MAX; + uint32_t table_source = 0; uint32_t selector_stride = 0; uint32_t selector_offset = 0; uint32_t key_arg = 0; + uint32_t table_offset = 0; + uint32_t key_count = 0; bool operator==(const IndirectImage& other) const = default; }; diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index 480ab6b1..88fec815 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -166,16 +166,15 @@ bool ReadSpecializationWord(const SrtRuntime& runtime, uint64_t address, uint32_ runtime.read_specialization_memory(runtime.userdata, address, &word); } -bool ReadScalarBufferWord(const ShaderBufferResource& descriptor, uint32_t dynamic_offset, - uint32_t immediate_offset, const SrtRuntime& runtime, uint32_t& word) { +bool ReadScalarTableWord(uint64_t base, uint64_t size, uint32_t dynamic_offset, + uint32_t immediate_offset, const SrtRuntime& runtime, uint32_t& word) { const auto byte_offset = static_cast(dynamic_offset) + immediate_offset; const auto aligned = byte_offset & ~uint64_t {3}; - const auto size = descriptor.GetSize(); if (aligned > size || size - aligned < sizeof(uint32_t)) { word = 0; return true; } - const auto base = descriptor.Base48() & ~uint64_t {3}; + base &= AddressMask & ~uint64_t {3}; if (aligned > AddressMask - base) { return false; } @@ -189,39 +188,59 @@ bool ReadScalarBufferWord(const ShaderBufferResource& descriptor, uint32_t dynam bool MaterializeIndirectImage(const ResourcePlan& program, const DescriptorSource::IndirectImage& indirect, const DescriptorValue& material_value, - const DescriptorValue& heap_value, uint32_t image_index, + const DescriptorValue& table_value, uint32_t image_index, const SrtRuntime& runtime, ResourceSnapshot& snapshot, ResourceSpecialization& specialization) { - ShaderBufferResource material; - ShaderBufferResource heap; - if (!DecodeBufferDescriptor(material_value, material) || - !DecodeBufferDescriptor(heap_value, heap) || material.Stride() != indirect.selector_stride) { - return false; - } - // Enumerate every wrapped scalar-buffer offset that can pass the descriptor bounds. - const auto step = std::gcd(indirect.selector_stride, uint64_t {1} << 32u); - const auto residue = static_cast(indirect.selector_offset) % step; - const auto limit = std::min(UINT32_MAX, material.GetSize() + 3u); - const auto probe_count = residue <= limit ? (limit - residue) / step + 1u : 0u; - if (probe_count > MaxIndirectImageProbes) { + uint64_t table_base = 0; + uint64_t table_size = UINT64_MAX; // Scalar addresses have no buffer descriptor bounds. + ShaderBufferResource table; + if (table_value.dword_count == 2u) { + table_base = (static_cast(table_value.dwords[1]) << 32u) | table_value.dwords[0]; + } else if (DecodeBufferDescriptor(table_value, table)) { + table_base = table.Base48(); + table_size = table.GetSize(); + } else { return false; } auto& keys = program.material_keys; keys.clear(); - keys.reserve(static_cast(probe_count) + 1u); - keys.push_back(0u); - for (uint64_t offset = residue; offset <= limit && probe_count != 0u; offset += step) { - uint32_t key = 0; - if (!ReadScalarBufferWord(material, static_cast(offset), 0u, runtime, key)) { + if (indirect.material_source == UINT32_MAX) { + if (table_value.dword_count != 2u || indirect.key_count == 0u || + indirect.key_count > MaxIndirectImageProbes) { return false; } - keys.push_back(key); - if (limit - offset < step) { - break; + keys.resize(indirect.key_count); + std::iota(keys.begin(), keys.end(), 0u); + } else { + ShaderBufferResource material; + if (!DecodeBufferDescriptor(material_value, material) || table_value.dword_count != 4u || + material.Stride() != indirect.selector_stride) { + return false; } + // Enumerate every wrapped scalar-buffer offset that can pass the descriptor bounds. + const auto step = std::gcd(indirect.selector_stride, uint64_t {1} << 32u); + const auto residue = static_cast(indirect.selector_offset) % step; + const auto limit = std::min(UINT32_MAX, material.GetSize() + 3u); + const auto probe_count = residue <= limit ? (limit - residue) / step + 1u : 0u; + if (probe_count > MaxIndirectImageProbes) { + return false; + } + keys.reserve(static_cast(probe_count) + 1u); + keys.push_back(0u); + for (uint64_t offset = residue; offset <= limit && probe_count != 0u; offset += step) { + uint32_t key = 0; + if (!ReadScalarTableWord(material.Base48(), material.GetSize(), + static_cast(offset), 0u, runtime, key)) { + return false; + } + keys.push_back(key); + if (limit - offset < step) { + break; + } + } + std::ranges::sort(keys); + keys.erase(std::unique(keys.begin(), keys.end()), keys.end()); } - std::ranges::sort(keys); - keys.erase(std::unique(keys.begin(), keys.end()), keys.end()); const auto children_begin = snapshot.images.size(); const auto mapping_offset = snapshot.flattened_srt.size(); @@ -232,10 +251,10 @@ bool MaterializeIndirectImage(const ResourcePlan& program, const auto key = keys[entry]; DescriptorValue candidate; candidate.dword_count = 8u; - const auto heap_offset = key << 5u; + const auto table_offset = (key << 5u) + indirect.table_offset; for (uint32_t dword = 0; dword < candidate.dword_count; ++dword) { - if (!ReadScalarBufferWord(heap, heap_offset, dword * sizeof(uint32_t), runtime, - candidate.dwords[dword])) { + if (!ReadScalarTableWord(table_base, table_size, table_offset, dword * sizeof(uint32_t), + runtime, candidate.dwords[dword])) { return false; } } @@ -816,7 +835,7 @@ ResourcePlan ExtractResourcePlan(const Program& program) { plan.requires_specialization_memory = true; MarkCleanFlatSlots(plan, Source(plan, source->indirect_image->material_source), plan.clean_flat_slots); - MarkCleanFlatSlots(plan, Source(plan, source->indirect_image->heap_source), + MarkCleanFlatSlots(plan, Source(plan, source->indirect_image->table_source), plan.clean_flat_slots); } return plan; @@ -892,10 +911,11 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime } const auto& indirect = *source->indirect_image; DescriptorValue material; - DescriptorValue heap; - if (!clean.EvaluateDescriptor(indirect.material_source, material) || - !clean.EvaluateDescriptor(indirect.heap_source, heap) || - !MaterializeIndirectImage(program, indirect, material, heap, i, runtime, snapshot, + DescriptorValue table; + if ((indirect.material_source != UINT32_MAX && + !clean.EvaluateDescriptor(indirect.material_source, material)) || + !clean.EvaluateDescriptor(indirect.table_source, table) || + !MaterializeIndirectImage(program, indirect, material, table, i, runtime, snapshot, specialization)) { return false; } diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp index c9b517b5..7ef6678b 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp @@ -344,16 +344,26 @@ private: } const MemoryInfo* ScalarReadMemory(const Inst& read, uint32_t& index) const { - if (read.GetOpcode() != ValueOpcode::ReadConstBuffer || read.NumArgs() != 2u) { + const bool address = read.GetOpcode() == ValueOpcode::LoadAddressU32; + if (!(address ? read.NumArgs() == 4u + : read.GetOpcode() == ValueOpcode::ReadConstBuffer && read.NumArgs() == 2u)) { return nullptr; } + if (address) { + const auto high = read.Arg(2).Resolve(); + const auto enabled = read.Arg(3).Resolve(); + if (!high.IsImmediate() || high.GetType() != Type::U32 || high.U32() != 0u || + !enabled.IsImmediate() || enabled.GetType() != Type::U1 || !enabled.U1()) { + return nullptr; + } + } index = read.Flags().index; if (index >= m_program.memory_info.size()) { return nullptr; } const auto& memory = m_program.memory_info[index]; - return memory.kind == ResourceKind::ScalarBuffer && memory.data_bits == 32u && - memory.data_dwords == 1u + return memory.kind == (address ? ResourceKind::ScalarAddress : ResourceKind::ScalarBuffer) && + memory.data_bits == 32u && memory.data_dwords == 1u ? &memory : nullptr; } @@ -376,18 +386,16 @@ private: return true; } - bool MakeRuntimeBufferSource(const Inst& handle, uint32_t pc, uint32_t& source, - DescriptorSource& descriptor) { - if (handle.GetOpcode() != ValueOpcode::GetBufferResource) { + bool MakeRuntimeTableSource(const Inst& handle, uint32_t pc, + DescriptorSource& descriptor) { + const auto width = handle.GetOpcode() == ValueOpcode::GetBufferResource ? 4u + : handle.GetOpcode() == ValueOpcode::GetAddressResource ? 2u : 0u; + if (width == 0u) { return false; } - MakeSource(handle, 4u, false, false, descriptor, pc); + MakeSource(handle, width, false, false, descriptor, pc); uint32_t bad_dword = 0; - if (!ValidateSource(descriptor, bad_dword)) { - return false; - } - source = InternSource(descriptor); - return true; + return ValidateSource(descriptor, bad_dword); } bool MatchMaterialOffset(Value value, Value& selector, uint32_t& stride, @@ -424,106 +432,183 @@ private: selector_inst->GetOpcode() == ValueOpcode::ReadFirstLane; } + bool NonzeroOnEntry(Value value, const Block* block) const { + if (m_program.blocks.size() != m_program.block_info.size()) { + return false; + } + // Each unique predecessor must execute before this use. Stop at joins: an + // unrelated comparison is not a bound on FindILsb's zero-input sentinel. + for (size_t depth = 0; block != nullptr && depth < m_program.blocks.size(); ++depth) { + if (block->ImmPredecessors().size() != 1u) { + return false; + } + const auto* previous = block->ImmPredecessors()[0]; + const auto current_it = std::ranges::find(m_program.blocks, block); + const auto previous_it = std::ranges::find(m_program.blocks, previous); + if (current_it == m_program.blocks.end() || previous_it == m_program.blocks.end()) { + return false; + } + const auto& info = m_program.block_info[previous_it - m_program.blocks.begin()]; + const auto id = m_program.block_info[current_it - m_program.blocks.begin()].id; + const auto& term = info.terminator; + if (term.kind == CFG::TerminatorKind::ConditionalBranch && + (term.true_block == id) != (term.false_block == id)) { + bool positive = term.true_block == id; + const auto* test = info.condition.Resolve().TryInstruction(); + while (test != nullptr && test->GetOpcode() == ValueOpcode::LogicalNot) { + positive = !positive; + test = test->Arg(0).Resolve().TryInstruction(); + } + if (test != nullptr && test->NumArgs() == 2u && + ((test->GetOpcode() == ValueOpcode::INotEqual32 && positive) || + (test->GetOpcode() == ValueOpcode::IEqual32 && !positive))) { + for (uint32_t arg = 0; arg < 2u; ++arg) { + uint32_t immediate; + if (ImmediateU32(test->Arg(arg), immediate) && immediate == 0u && + EquivalentValue(m_program, test->Arg(arg ^ 1u), value)) { + return true; + } + } + } + } + block = previous; + } + return false; + } + + bool MatchTableOffset(Value value, Value& key, uint32_t& offset) const { + offset = 0; + for (;;) { + const auto* inst = value.Resolve().TryInstruction(); + if (inst == nullptr || inst->NumArgs() != 2u) { + return false; + } + uint32_t immediate; + if (inst->GetOpcode() == ValueOpcode::ShiftLeftLogical32 && + ImmediateU32(inst->Arg(1), immediate) && immediate == 5u) { + key = inst->Arg(0).Resolve(); + return key.GetType() == Type::U32; + } + if (inst->GetOpcode() != ValueOpcode::IAdd32) { + return false; + } + if (ImmediateU32(inst->Arg(0), immediate)) { + value = inst->Arg(1); + } else if (ImmediateU32(inst->Arg(1), immediate)) { + value = inst->Arg(0); + } else { + return false; + } + // These additions are shader U32 arithmetic, before the scalar memory offset. + offset += immediate; + } + } + bool TryMakeIndirectImage(Inst& handle, uint32_t pc, IndirectImagePlan& plan) { if (handle.GetOpcode() != ValueOpcode::GetImageResource || handle.NumArgs() != 8u) { return false; } - - std::array heap_reads {}; - Inst* heap_handle = nullptr; - Value heap_offset; - for (uint32_t dword = 0; dword < heap_reads.size(); dword++) { - heap_reads[dword] = handle.Arg(dword).Resolve().TryInstruction(); - if (heap_reads[dword] == nullptr) { + Inst* table_handle = nullptr; + Value key; + uint32_t table_offset = 0; + for (uint32_t dword = 0; dword < plan.reads.size(); ++dword) { + auto* read = handle.Arg(dword).Resolve().TryInstruction(); + if (read == nullptr) { return false; } - uint32_t memory_index = 0; - const auto* memory = ScalarReadMemory(*heap_reads[dword], memory_index); - if (memory == nullptr || memory->offset != dword * sizeof(uint32_t) || - !MemoryIndexBelongsTo(memory_index, *heap_reads[dword])) { + uint32_t memory_index = 0; + const auto* memory = ScalarReadMemory(*read, memory_index); + if (memory == nullptr || memory->offset > INT32_MAX || (memory->offset & 3u) != 0u || + !MemoryIndexBelongsTo(memory_index, *read)) { return false; } - auto* current_handle = heap_reads[dword]->Arg(0).Resolve().TryInstruction(); + auto* current_handle = read->Arg(0).Resolve().TryInstruction(); + Value current_key; + uint32_t offset = 0; if (current_handle == nullptr || - (heap_handle != nullptr && current_handle != heap_handle)) { + current_handle->GetOpcode() != (memory->kind == ResourceKind::ScalarAddress + ? ValueOpcode::GetAddressResource + : ValueOpcode::GetBufferResource) || + (memory->kind == ResourceKind::ScalarAddress && read->Parent() != handle.Parent()) || + (table_handle != nullptr && + !EquivalentValue(m_program, Value(table_handle), Value(current_handle))) || + !MatchTableOffset(read->Arg(1), current_key, offset) || + memory->offset > UINT32_MAX - offset) { return false; } - heap_handle = current_handle; + offset += memory->offset; if (dword == 0u) { - heap_offset = heap_reads[dword]->Arg(1).Resolve(); - } else if (!EquivalentValue(m_program, heap_offset, heap_reads[dword]->Arg(1))) { + key = current_key; + table_offset = offset; + } else if (!EquivalentValue(m_program, key, current_key) || + static_cast(table_offset) + dword * sizeof(uint32_t) != offset) { return false; } - plan.memory[dword] = memory_index; - plan.reads[dword] = heap_reads[dword]; - } - - const auto* shift = heap_offset.TryInstruction(); - uint32_t shift_amount = 0; - if (shift == nullptr || shift->GetOpcode() != ValueOpcode::ShiftLeftLogical32 || - shift->NumArgs() != 2u || !ImmediateU32(shift->Arg(1), shift_amount) || - shift_amount != 5u) { - return false; - } - auto* material_read = shift->Arg(0).Resolve().TryInstruction(); - if (material_read == nullptr) { - return false; - } - uint32_t material_memory_index = 0; - const auto* material_memory = ScalarReadMemory(*material_read, material_memory_index); - if (material_memory == nullptr || material_memory->offset != 0u || - !MemoryIndexBelongsTo(material_memory_index, *material_read)) { - return false; - } - auto* material_handle = material_read->Arg(0).Resolve().TryInstruction(); - if (material_handle == nullptr) { - return false; - } - - Value selector; - uint32_t selector_stride = 0; - uint32_t selector_offset = 0; - if (!MatchMaterialOffset(material_read->Arg(1), selector, selector_stride, - selector_offset)) { - return false; - } - - const std::array material_users {shift}; - std::array heap_users {}; - std::copy(heap_reads.begin(), heap_reads.end(), heap_users.begin()); - const std::array image_users {&handle}; - if (!UsesOnly(*material_read, material_users) || !UsesOnly(*shift, heap_users)) { - return false; - } - for (const auto* read: heap_reads) { + table_handle = current_handle; + const std::array image_users {&handle}; if (!UsesOnly(*read, image_users)) { return false; } + plan.memory[dword] = memory_index; + plan.reads[dword] = read; } - DescriptorSource material_source; - DescriptorSource heap_source; - uint32_t material_source_index = 0; - uint32_t heap_source_index = 0; - if (!MakeRuntimeBufferSource(*material_handle, pc, material_source_index, - material_source) || - !MakeRuntimeBufferSource(*heap_handle, pc, heap_source_index, heap_source)) { + DescriptorSource table_source; + if (!MakeRuntimeTableSource(*table_handle, pc, table_source)) { return false; } - + DescriptorSource material_source; + DescriptorSource::IndirectImage indirect; + indirect.table_offset = table_offset; + if (table_source.dword_count == 2u) { + const auto* selector = key.Resolve().TryInstruction(); + if (m_shader_writes || selector == nullptr || + selector->GetOpcode() != ValueOpcode::FindILsb32 || selector->NumArgs() != 1u || + !NonzeroOnEntry(selector->Arg(0), handle.Parent()) || + (table_offset & 3u) != 0u || table_offset > UINT32_MAX - (32u * 32u - 1u)) { + return false; + } + indirect.key_count = 32u; + } else { + auto* material_read = key.Resolve().TryInstruction(); + uint32_t material_memory_index = 0; + const auto* memory = material_read != nullptr + ? ScalarReadMemory(*material_read, material_memory_index) : nullptr; + if (table_offset != 0u || memory == nullptr || memory->kind != ResourceKind::ScalarBuffer || + memory->offset != 0u || !MemoryIndexBelongsTo(material_memory_index, *material_read)) { + return false; + } + Value selector; + if (!MatchMaterialOffset(material_read->Arg(1), selector, indirect.selector_stride, + indirect.selector_offset)) { + return false; + } + const auto* shift = plan.reads[0]->Arg(1).Resolve().TryInstruction(); + const std::array material_users {shift}; + if (!UsesOnly(*material_read, material_users) || !UsesOnly(*shift, plan.reads)) { + return false; + } + const auto* material_handle = material_read->Arg(0).Resolve().TryInstruction(); + if (material_handle == nullptr || + material_handle->GetOpcode() != ValueOpcode::GetBufferResource || + !MakeRuntimeTableSource(*material_handle, pc, material_source)) { + return false; + } + indirect.material_source = InternSource(material_source); + } + indirect.table_source = InternSource(table_source); DescriptorSource image_source; image_source.dword_count = 8u; - std::copy(material_source.dwords.begin(), material_source.dwords.begin() + 4u, - image_source.dwords.begin()); - std::copy(heap_source.dwords.begin(), heap_source.dwords.begin() + 4u, - image_source.dwords.begin() + 4u); - image_source.indirect_image = DescriptorSource::IndirectImage { - material_source_index, heap_source_index, selector_stride, selector_offset, 0u}; - + image_source.dwords.fill(Value(0u)); + std::copy_n(material_source.dwords.begin(), material_source.dword_count, + image_source.dwords.begin()); + std::copy_n(table_source.dwords.begin(), table_source.dword_count, + image_source.dwords.begin() + 4u); + image_source.indirect_image = indirect; plan.handle = &handle; plan.source = InternSource(image_source); - plan.key = Value(material_read); - plan.roots = image_source.dwords; + plan.key = key; + plan.roots = image_source.dwords; return true; } diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index ce2f5b44..7755c1b4 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -506,6 +506,139 @@ void TestInvariantIndirectImageMaterialization() { "wrapped scalar immediate entered the invariant image proof"); } +void TestGuardedDirectImageTable() { + namespace CFG = Libs::Graphics::ShaderRecompiler::CFG; + enum class Guard { Nonzero, Zero, Unrelated, Bypass }; + const auto make_plan = [](Guard guard) { + Fixture fixture(ShaderType::Pixel); + auto *entry = fixture.block; + auto *middle = fixture.AddBlock(); + auto *before_sample = fixture.AddBlock(); + auto *sample = fixture.AddBlock(); + auto *exit = fixture.AddBlock(); + entry->AddBranch(middle); + entry->AddBranch(exit); + middle->AddBranch(before_sample); + before_sample->AddBranch(sample); + sample->AddBranch(exit); + if (guard == Guard::Bypass) exit->AddBranch(sample); + const auto mask = fixture.Emit(ValueOpcode::ReadFirstLane, + {fixture.Emit(ValueOpcode::GetAttribute, {Value(0u), Value(0u)}), Value(true)}); + const auto nonzero = fixture.Emit( + ValueOpcode::INotEqual32, + {Value(0u), guard == Guard::Unrelated ? fixture.UserData(2) : mask}); + fixture.program.block_info[0].condition = + fixture.Emit(ValueOpcode::LogicalNot, {nonzero}); + fixture.program.block_info[0].terminator = { + .kind = CFG::TerminatorKind::ConditionalBranch, + .true_block = guard == Guard::Zero ? 1u : 4u, + .false_block = guard == Guard::Zero ? 4u : 1u}; + for (uint32_t block = 1; block < 4; ++block) { + fixture.program.block_info[block].terminator = { + .kind = CFG::TerminatorKind::Branch, .true_block = block + 1u}; + } + fixture.program.block_info[4].terminator = { + .kind = guard == Guard::Bypass ? CFG::TerminatorKind::Branch + : CFG::TerminatorKind::Return, + .true_block = 3u}; + const auto srt = fixture.Address(fixture.UserData(0), fixture.UserData(1)); + std::array pointer; + for (uint32_t word = 0; word < pointer.size(); ++word) { + MemoryInfo memory; + memory.kind = ResourceKind::ScalarAddress; + memory.offset = word * 4u; + pointer[word] = fixture.Emit( + ValueOpcode::LoadAddressU32, {srt, Value(0u), Value(0u), Value(true)}, + fixture.AddMemory(memory, 0x20)); + } + fixture.block = sample; + const auto key = fixture.Emit(ValueOpcode::FindILsb32, {mask}); + const auto offset = fixture.Emit(ValueOpcode::IAdd32, + {fixture.Emit(ValueOpcode::ShiftLeftLogical32, {key, Value(5u)}), + Value(344u)}); + std::array words; + for (uint32_t group = 0; group < 2u; ++group) { + // Separate equivalent pointer handles mirror the two scalar x4 loads. + const auto table = fixture.Address(pointer[0], pointer[1]); + const auto group_offset = group == 0u ? offset : fixture.Emit( + ValueOpcode::IAdd32, {offset, Value(16u)}); + for (uint32_t word = 0; word < 4u; ++word) { + MemoryInfo memory; + memory.kind = ResourceKind::ScalarAddress; + memory.offset = word * 4u; + words[group * 4u + word] = fixture.Emit( + ValueOpcode::LoadAddressU32, + {table, group_offset, Value(0u), Value(true)}, + fixture.AddMemory(memory, 0x100 + group * 8u)); + } + } + const auto image = fixture.Image(words, 0x128); + const auto sampler = fixture.Sampler({Value(0u), Value(0u), Value(0u), Value(0u)}); + MemoryInfo memory; + memory.kind = ResourceKind::Image; + memory.image_dimension = Decoder::ImageDimension::Dim2D; + fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()}, + fixture.AddMemory(memory, 0x128)); + fixture.PlanAndTrack(); + const auto source = fixture.program.info.images[0].source; + const auto &indirect = fixture.program.descriptor_sources[source].indirect_image; + Check(indirect && indirect->material_source == UINT32_MAX && + indirect->selector_stride == 0u && indirect->table_offset == 344u && + indirect->key_count == 32u && + fixture.program.descriptor_sources[indirect->table_source].dword_count == 2u, + "guarded direct image table lost its pointer or proven selector range"); + return ExtractResourcePlan(fixture.program); + }; + + auto plan = make_plan(Guard::Nonzero); + for (const auto guard : {Guard::Zero, Guard::Unrelated, Guard::Bypass}) { + CheckFatal([&] { make_plan(guard); }, "not a valid runtime value", + "direct table accepted a selector without a dominating nonzero guard"); + } + LinearTestMemory memory; + constexpr uint64_t table = 0x1800u + 344u; + memory.words[0] = 0x1800u; + const auto fill_table = [](LinearTestMemory &memory, uint64_t base) { + for (uint32_t key = 0; key < 32u; ++key) { + const auto word = (base - memory.base) / 4u + key * 8u; + memory.words[word] = 0x100u + key; + memory.words[word + 1u] = static_cast( + Libs::Graphics::Prospero::BufferFormat::k32_32_32_32Float) << 20u; + memory.words[word + 3u] = Libs::Graphics::DstSel(4, 5, 6, 7) | + (static_cast(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u); + } + }; + fill_table(memory, table); + const std::array user_data{0x1000u, 0u}; + SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory, + .userdata = &memory, + .read_specialization_memory = ReadLinearTestMemory}; + ResourceSnapshot snapshot; + ResourceSpecialization specialization; + Check(MaterializeResources(plan, runtime, snapshot, specialization) && + snapshot.images.size() == 32u && specialization.images.size() == 32u && + snapshot.flattened_srt[specialization.images[0].indirect_mapping_offset] == 32u, + "direct table did not retain all 32 reachable descriptors"); + memory.words[(table - memory.base) / 4u + 31u * 8u] = 0x987u; + Check(MaterializeResources(plan, runtime, snapshot, specialization) && + snapshot.images[31].dwords[0] == 0x987u, + "direct table refresh reused stale descriptor contents"); + memory.fail_address = table + 31u * 32u + 28u; + Check(!MaterializeResources(plan, runtime, snapshot, specialization), + "direct table accepted an unreadable final descriptor word"); + + LinearTestMemory wrapping; + wrapping.base = 0u; + wrapping.words[0x1000u / 4u] = 0xffffff00u; + wrapping.words[0x1000u / 4u + 1u] = 0xffffu; + wrapping.watched_address = 88u; + fill_table(wrapping, 88u); + runtime.userdata = &wrapping; + Check(!MaterializeResources(plan, runtime, snapshot, specialization) && + wrapping.watched_reads == 0u, + "direct table wrapped its descriptor address at the 48-bit boundary"); +} + void TestUniformScalarBufferImage() { Fixture fixture(ShaderType::Pixel); std::array material_words; @@ -2232,6 +2365,7 @@ int main() { Run("FMASK load specialization", TestFmaskLoadSpecialization); Run("dynamic storage mips", TestDynamicStorageMipTracking); Run("invariant indirect images", TestInvariantIndirectImageMaterialization); + Run("guarded direct image table", TestGuardedDirectImageTable); Run("draw-uniform scalar image", TestUniformScalarBufferImage); Run("SRT runtime", TestSrtFlatteningAndRuntimeMemoization); Run("dynamic SRT", TestDynamicSrtReadRemainsExplicit);