From 487bbf95fd960978d764d58a938235b2d47c1cbc Mon Sep 17 00:00:00 2001 From: nmzik Date: Wed, 14 Oct 2026 16:40:15 +0200 Subject: [PATCH] shader: share nested pointer-table image and sampler selection Co-authored-by: fxpw --- .../backend/spirv/spirvEmitterImage.cpp | 7 +- src/graphics/shader/recompiler/ir/ShaderIR.h | 12 ++ .../ir/passes/ResourceMaterialization.cpp | 80 +++++-- .../ir/passes/ResourceMaterialization.h | 8 + .../recompiler/ir/passes/ResourceTracking.cpp | 97 ++++++++- tests/ResourceTrackingTests.cpp | 196 ++++++++++++++++++ tests/ShaderRecompilerComputeTests.cpp | 111 ++++++++++ 7 files changed, 484 insertions(+), 27 deletions(-) diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp index d715aeb56..ea8348279 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp @@ -826,8 +826,13 @@ std::array LoadImageSamplers(ValueEmitContext& ctx, const IR::I const auto* handle = inst.Arg(1).ResolveInstruction(); if (handle == nullptr || handle->GetOpcode() != IR::ValueOpcode::GetSamplerResource) ctx.Fail(inst, "has invalid finite sampler key provenance"); + auto selected = ctx.Def(handle->Arg(0)); + if (samplers[index].indirect_root != IR::SamplerResource::NoIndirectSampler) + selected = EmitIndirectResourceIndex(state, selected, + samplers[index].indirect_mapping_offset, + samplers[index].indirect_search_iterations, 0u); array_index = EmitIndexSwitch( - state, ctx.Def(handle->Arg(0)), static_cast(candidates.size()), + state, selected, static_cast(candidates.size()), TypeU32(state), [&](uint32_t ordinal) { return ConstantU32(state, candidates[ordinal]); }); } sampler = {LoadSamplerDescriptor(state, index, array_index), array_index != 0u}; diff --git a/src/graphics/shader/recompiler/ir/ShaderIR.h b/src/graphics/shader/recompiler/ir/ShaderIR.h index e79883eed..425cee9f1 100644 --- a/src/graphics/shader/recompiler/ir/ShaderIR.h +++ b/src/graphics/shader/recompiler/ir/ShaderIR.h @@ -181,10 +181,15 @@ RawSignedStorageFormat(const ImageResource& image, Prospero::BufferFormat format } struct SamplerResource { + static constexpr uint32_t NoIndirectSampler = UINT32_MAX; + uint32_t source = 0; uint32_t first_use_pc = 0; // Native filtering/border variants share the original sampler's runtime descriptor. uint32_t snapshot_index = 0; + uint32_t indirect_root = NoIndirectSampler; + uint32_t indirect_mapping_offset = 0; + uint32_t indirect_search_iterations = 0; std::vector indirect_resources; bool force_point_filtering = false; bool depth_compare = false; @@ -520,6 +525,12 @@ struct ShaderInfo { struct DescriptorSource { struct IndirectDescriptor { + struct PointerTable { + uint32_t maximum = 0; + uint32_t offset = 0; + + bool operator==(const PointerTable&) const = default; + }; struct SelectorRead { struct CountedRows { uint32_t source = UINT32_MAX; @@ -542,6 +553,7 @@ struct DescriptorSource { bool operator==(const SelectorRead& other) const = default; }; std::optional selector; + std::optional pointer_table; uint32_t table_source = 0; uint32_t table_offset = 0; uint32_t table_immediate = 0; diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index 8b87bb2bb..510cd296c 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -127,7 +127,7 @@ bool ValidatePackedUnormSampling(const ResourcePlan& program, const ResourceSnap for (const auto& pair: program.info.sampled_pairs) { if (pair.image != image_index) continue; if (pair.sampler >= snapshot.samplers.size() || - !program.info.samplers[pair.sampler].indirect_resources.empty()) { + program.descriptor_sources[program.info.samplers[pair.sampler].source].indirect_descriptor) { return SpecializationFail("packed UNorm sampling requires a direct sampler"); } ShaderSamplerResource sampler; @@ -367,11 +367,16 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p (table.Stride() & 3u) != 0u || table.Stride() < indirect.table_record_bytes))) 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}) + + const auto* pointer = indirect.pointer_table ? &*indirect.pointer_table : nullptr; + const uint64_t step = pointer != nullptr ? indirect.table_stride + : affine ? uint64_t {1} << std::countr_zero(indirect.table_stride) : 4u; + const uint64_t first = ((pointer != nullptr ? indirect.table_offset + : indirect.table_offset % step) & ~uint64_t {3}) + (indirect.table_immediate & ~3u); + const uint64_t domain = pointer != nullptr ? uint64_t {pointer->maximum} + 1u + : (uint64_t {1} << 32u) / step; const auto count = table_size >= first + 4u - ? std::min((table_size - first - 4u) / step + 1u, (uint64_t {1} << 32u) / step) + ? std::min((table_size - first - 4u) / step + 1u, domain) : 0u; if (affine && count > MaxIndirectDescriptorProbes) return false; // Affine SMEM offsets wrap in U32 before adding the aligned immediate. @@ -385,7 +390,13 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p snapshot.flattened_srt.resize(mapping_offset + 2u, 0u); for (uint32_t word = 0; word < count; ++word) { DescriptorValue candidate {.dword_count = dword_count}; - if (affine) { + if (pointer != nullptr) { + std::array address {}; + if (!ReadScalarTable(table_base, table_size, first + word * step, runtime, address) || + !ReadScalarTable((uint64_t {address[1]} << 32u) | address[0], UINT64_MAX, + pointer->offset, runtime, + std::span(candidate.dwords).first(dword_count))) return false; + } else if (affine) { if (!ReadScalarTable(table_base, table_size, first + word * step, runtime, std::span(candidate.dwords).first(dword_count))) return false; } else { @@ -1437,18 +1448,28 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime } } snapshot.samplers.resize(program.info.samplers.size()); - for (uint32_t i = 0; i < program.info.samplers.size(); ++i) { - auto source = program.info.samplers[i].source; - if (source >= program.descriptor_sources.size()) return false; - const auto& descriptor = program.descriptor_sources[source]; - if (descriptor.indirect_descriptor) { - if (descriptor.indirect_descriptor->sources.empty()) return false; - source = descriptor.indirect_descriptor->sources[0]; + specialization.samplers.assign(program.info.samplers.size(), {}); + for (uint32_t i = 0; i < snapshot.samplers.size(); ++i) { + const auto base_index = + i < program.info.samplers.size() ? i : specialization.samplers[i].indirect_root; + if (base_index >= program.info.samplers.size()) return false; + if (i < program.info.samplers.size()) { + auto source = program.info.samplers[i].source; + const auto* descriptor = Source(program, source); + if (descriptor == nullptr) return false; + const auto* indirect = descriptor->indirect_descriptor ? &*descriptor->indirect_descriptor : nullptr; + if (indirect != nullptr && indirect->sources.empty()) { + snapshot.samplers[i] = {.dword_count = 4u}; + if ((active.empty() || active[source]) && !MaterializeIndirectDescriptor( + program, *indirect, i, 4u, observed, clean, snapshot, snapshot.samplers, + specialization.samplers, ShaderInfo::MaxSamplers, + [](DescriptorValue&) { return true; })) return false; + } else { + if (indirect != nullptr) source = indirect->sources[0]; + if (!evaluate(source, snapshot.samplers[i])) return false; + } } - if (!evaluate(source, snapshot.samplers[i])) { - return false; - } - if (program.info.samplers[i].gather_lod) { + if (program.info.samplers[base_index].gather_lod) { const auto control = snapshot.samplers[i].dwords[2]; const auto filter = (control >> 26u) & 3u; // MipNone always selects the base level. Explicit point gathers currently require @@ -1466,7 +1487,8 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime void ApplyResourceSpecialization(Program& program, const ResourceSpecialization& specialization) { EXIT_IF(!program.resource_tracking_complete || program.shader_info_complete); EXIT_IF(program.info.buffers.size() > specialization.buffers.size() || - program.info.images.size() > specialization.images.size()); + program.info.images.size() > specialization.images.size() || + program.info.samplers.size() > specialization.samplers.size()); auto& buffers = program.info.buffers; const auto original_buffer_count = buffers.size(); @@ -1522,10 +1544,30 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization& } } - SamplerPlan sampler_plan; - EXIT_IF(!BuildSamplerPlan(program.info, sampler_plan)); auto& samplers = program.info.samplers; auto& sampled_pairs = program.info.sampled_pairs; + const auto base_sampler_count = samplers.size(); + samplers.reserve(specialization.samplers.size()); + for (uint32_t index = 0; index < specialization.samplers.size(); ++index) { + const auto& source = specialization.samplers[index]; + if (index >= samplers.size()) { + EXIT_IF(source.indirect_root >= base_sampler_count); + samplers.push_back(samplers[source.indirect_root]); + samplers.back().indirect_resources.clear(); + } + samplers[index].indirect_root = source.indirect_root; + samplers[index].indirect_mapping_offset = source.indirect_mapping_offset; + samplers[index].indirect_search_iterations = source.indirect_search_iterations; + } + for (uint32_t index = 0; index < samplers.size(); ++index) { + const auto root = samplers[index].indirect_root; + if (root != SamplerResource::NoIndirectSampler) { + EXIT_IF(root >= samplers.size()); + samplers[root].indirect_resources.push_back(index); + } + } + SamplerPlan sampler_plan; + EXIT_IF(!BuildSamplerPlan(program.info, sampler_plan)); const auto original_sampler_count = samplers.size(); samplers.reserve(sampler_plan.sampler_count); for (uint32_t index = 0; index < sampler_plan.sampler_count; index++) { diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.h b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.h index b2b9706da..78a3b5f1b 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.h +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.h @@ -34,8 +34,16 @@ struct ResourceSpecialization { bool operator==(const Image&) const = default; }; + struct Sampler { + uint32_t indirect_root = SamplerResource::NoIndirectSampler; + uint32_t indirect_mapping_offset = 0; + uint32_t indirect_search_iterations = 0; + bool operator==(const Sampler&) const = default; + }; + std::vector buffers; std::vector images; + std::vector samplers; bool operator==(const ResourceSpecialization&) const = default; }; diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp index 8f98112e5..b04dded41 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp @@ -45,10 +45,13 @@ uint32_t PossibleU32Bits(Value value, const auto& normalize) { if (inst->NumArgs() != 0u) bits = merged; break; } - case ValueOpcode::ShiftLeftLogical32: { + case ValueOpcode::ShiftLeftLogical32: + case ValueOpcode::ShiftRightLogical32: { const auto shift = inst->Arg(1).Resolve(); - if (shift.IsImmediate() && shift.GetType() == Type::U32) - bits = arg(0) << (shift.U32() & 31u); + if (shift.IsImmediate() && shift.GetType() == Type::U32) { + bits = inst->GetOpcode() == ValueOpcode::ShiftLeftLogical32 + ? arg(0) << (shift.U32() & 31u) : arg(0) >> (shift.U32() & 31u); + } break; } default: break; @@ -397,6 +400,13 @@ public: std::vector> buffer_selections; for (const auto& plan: m_indirect_descriptors) { auto key = plan.key; + if (m_sources[plan.source].indirect_descriptor->pointer_table) { + auto* block = plan.handle->Parent(); + const auto where = std::ranges::find_if(block->Instructions(), + [&](const Inst& inst) { return &inst == plan.handle; }); + key = Value(&*block->PrependNewInst( + where, ValueOpcode::ShiftLeftLogical32, {key, Value(2u)})); + } if (plan.table_read != nullptr) { const auto* anchor = plan.table_indexed ? plan.handle : plan.table_read; auto* block = anchor->Parent(); @@ -964,7 +974,8 @@ private: if (current.indirect_descriptor.has_value()) { const auto& a = *current.indirect_descriptor; const auto& b = *descriptor.indirect_descriptor; - if (a.selector != b.selector || a.table_source != b.table_source || + if (a.selector != b.selector || a.pointer_table != b.pointer_table || + a.table_source != b.table_source || a.table_offset != b.table_offset || a.table_immediate != b.table_immediate || a.table_stride != b.table_stride || a.indexed != b.indexed || a.workgroup_axis != b.workgroup_axis || a.sources != b.sources || @@ -2713,6 +2724,74 @@ private: return true; } + bool TryMakeNestedDescriptor(Inst& handle, const DescriptorSource& descriptor, + IndirectDescriptorPlan& plan) { + if (handle.GetOpcode() != ValueOpcode::GetImageResource && + handle.GetOpcode() != ValueOpcode::GetSamplerResource) return false; + const Inst* pointer = nullptr; + uint32_t descriptor_offset = 0; + for (uint32_t word = 0; word < handle.NumArgs(); ++word) { + const auto* read = descriptor.dwords[word].Resolve().TryInstruction(); + uint32_t index, offset; + const auto* memory = read != nullptr ? ScalarReadMemory(*read, index) : nullptr; + if (memory == nullptr || memory->kind != ResourceKind::ScalarAddress || + memory->offset > INT32_MAX || !ImmediateU32(read->Arg(1), offset) || + !AvailableBefore(*read, handle)) return false; + const auto* address = read->Arg(0).Resolve().TryInstruction(); + const uint64_t current_offset = uint64_t {offset & ~3u} + (memory->offset & ~3u); + if (address == nullptr || address->GetOpcode() != ValueOpcode::GetAddressResource || + address->NumArgs() != 2u || current_offset > UINT32_MAX || + (pointer != nullptr && (!EquivalentValue(m_program, Value(pointer), Value(address)) || + current_offset != uint64_t {descriptor_offset} + word * 4u))) + return false; + if (word == 0u) descriptor_offset = static_cast(current_offset); + pointer = address; + } + // An out-of-bounds pointer pair is zero. Guest mappings exclude the first + // 16 KiB page, so these pointed reads have the existing null-image semantics. + if (pointer == nullptr || uint64_t {descriptor_offset} + handle.NumArgs() * 4u > 0x4000u) + return false; + const auto* low = pointer->Arg(0).Resolve().TryInstruction(); + const auto* high = pointer->Arg(1).Resolve().TryInstruction(); + uint32_t low_index, high_index; + const auto* low_memory = low != nullptr ? ScalarReadMemory(*low, low_index) : nullptr; + const auto* high_memory = high != nullptr ? ScalarReadMemory(*high, high_index) : nullptr; + if (low_memory == nullptr || high_memory == nullptr || + low_memory->kind != ResourceKind::ScalarBuffer || + high_memory->kind != ResourceKind::ScalarBuffer || + low_memory->offset > INT32_MAX || high_memory->offset > INT32_MAX || + (low_memory->offset & ~3u) + 4u != (high_memory->offset & ~3u) || + !EquivalentValue(m_program, low->Arg(0), high->Arg(0)) || + !EquivalentValue(m_program, low->Arg(1), high->Arg(1)) || + !AvailableBefore(*low, *pointer) || !AvailableBefore(*high, *pointer)) return false; + Value key; + DescriptorSource::IndirectDescriptor indirect; + if (!MatchTableOffset(low->Arg(1), key, indirect.table_offset, indirect.table_stride) || + indirect.table_stride < 8u || (indirect.table_stride & 3u) != 0u) return false; + const auto maximum = SelectorMaximum(key); + if (maximum > UINT32_MAX / 4u || + uint64_t {maximum} * indirect.table_stride + indirect.table_offset > UINT32_MAX || + (key.TryInstruction() != nullptr && !AvailableBefore(*key.Instruction(), handle))) + return false; + DescriptorSource table_source; + if (!MakeRuntimeTableSource(*low, table_source) || table_source.dword_count != 4u) + return false; + indirect.table_source = InternSource(table_source); + indirect.table_immediate = low_memory->offset & ~3u; + indirect.table_scalar = true; + indirect.pointer_table = {{maximum, descriptor_offset}}; + DescriptorSource source; + source.dword_count = handle.NumArgs(); + source.dwords.fill(Value(0u)); + std::copy_n(table_source.dwords.begin(), 4u, source.dwords.begin()); + source.indirect_descriptor = indirect; + plan.handle = &handle; + plan.source = InternSource(source); + plan.key = key; + plan.roots = source.dwords; + return true; + } + bool TryMakeIndirectImage(Inst& handle, const DescriptorSource& descriptor, IndirectDescriptorPlan& plan) { if (handle.GetOpcode() != ValueOpcode::GetImageResource || handle.NumArgs() != 8u) return false; @@ -3019,6 +3098,9 @@ private: return SelectorMaximum(inst->Arg(arg), bounds, depth + 1u, guard); }; switch (inst->GetOpcode()) { + case ValueOpcode::ReadLane: + // ReadLane ignores EXEC; only context-free bounds on every source lane apply. + return PossibleU32Bits(inst->Arg(0)); case ValueOpcode::Phi: { const auto initial = InitialCandidateMask(value); if (!initial.IsEmpty()) return SelectorMaximum(initial, bounds, depth + 1u, guard); @@ -3240,10 +3322,11 @@ private: uint32_t bad_dword = 0; if (ValidateSource(descriptor, bad_dword)) return; IndirectDescriptorPlan plan; - if (sampler ? TryMakeFiniteDescriptor(*handle, plan) + if (TryMakeNestedDescriptor(*handle, descriptor, plan) || + (sampler ? TryMakeFiniteDescriptor(*handle, plan) : (TryMakeIndirectImage(*handle, descriptor, plan) || TryMakeFiniteDescriptor(*handle, plan) || - TryMakeIndirectBuffer(*handle, descriptor, plan))) { + TryMakeIndirectBuffer(*handle, descriptor, plan)))) { for (const auto& previous: m_indirect_descriptors) { if (plan.reads[0] != nullptr && previous.reads[0] == plan.reads[0] && !EquivalentValue(m_program, previous.key, plan.key)) @@ -3389,7 +3472,7 @@ private: const auto* descriptor = Source(source); if (descriptor != nullptr && descriptor->indirect_descriptor) { const auto& sources = descriptor->indirect_descriptor->sources; - if (sources.empty()) Fail(pc, "finite sampler has no candidates"); + if (sources.empty()) return root; std::vector resources {root}; for (size_t i = 1; i < sources.size(); ++i) { const auto candidate = AddSampler(sources[i], pc); diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index f2bb4fddf..8c6d039a1 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -561,6 +561,201 @@ void TestWaterfallImageTable() { "waterfall without all descriptor equality witnesses was accepted"); } +void TestNestedPointerImageTable() { + namespace CFG = Libs::Graphics::ShaderRecompiler::CFG; + const auto make = [](uint32_t variant = 0u) { + auto fixture = std::make_unique(); + std::array table_words, key_words; + for (uint32_t word = 0; word < 4u; ++word) { + table_words[word] = fixture->UserData(word); + key_words[word] = fixture->UserData(word + 4u); + } + const auto table = fixture->Buffer(table_words); + const auto keys = fixture->Buffer(key_words); + Inst* carried = nullptr; + auto* entry = fixture->block; + if (variant == 6u) { + auto* loop = fixture->AddBlock(); + auto* exit = fixture->AddBlock(); + entry->AddBranch(loop); + entry->terminator = {.kind = CFG::TerminatorKind::Branch, + .true_block = loop}; + loop->AddBranch(loop); + loop->AddBranch(exit); + loop->terminator = {.kind = CFG::TerminatorKind::ConditionalBranch, + .true_block = loop, .false_block = exit}; + exit->terminator.kind = CFG::TerminatorKind::Return; + fixture->block = loop; + carried = &loop->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32)); + } + const auto active = fixture->Emit(ValueOpcode::INotEqual32, + {fixture->Emit(ValueOpcode::LaneId), Value(0u)}); + MemoryInfo key_memory; + key_memory.kind = ResourceKind::Buffer; + key_memory.offen = true; + auto key = fixture->Emit(ValueOpcode::LoadBufferU32, + {keys, Value(0u), fixture->Emit(ValueOpcode::LaneId), Value(0u), Value(true)}, + fixture->AddMemory(key_memory, 0x100u)); + if (carried != nullptr) { + carried->AddPhiOperand(entry, Value(0u)); + carried->AddPhiOperand(fixture->block, + fixture->Emit(ValueOpcode::SelectU32, {active, key, Value(carried)})); + fixture->block->condition = active; + key = Value(carried); + } + const auto unbounded_key = key; + if (variant != 1u) + key = fixture->Emit(ValueOpcode::ShiftRightLogical32, {key, Value(variant == 4u ? 15u : 16u)}); + if (variant == 5u || variant == 6u) + key = fixture->Emit(ValueOpcode::SelectU32, + {active, key, variant == 5u ? unbounded_key : Value(0u)}); + key = fixture->Emit(ValueOpcode::ReadLane, {key, Value(0u)}); + const auto offset = fixture->Emit(ValueOpcode::IMul32, {key, Value(592u)}); + std::array pointer; + for (uint32_t word = 0; word < 2u; ++word) { + MemoryInfo memory; + memory.kind = ResourceKind::ScalarBuffer; + memory.offset = word * (variant == 2u ? 8u : 4u); + pointer[word] = fixture->Emit(ValueOpcode::ReadConstBuffer, {table, offset}, + fixture->AddMemory(memory, 0x1f4u)); + } + const auto address = fixture->Address(pointer[0], pointer[1]); + const auto load = [&](uint32_t first, uint32_t width) { + if (width == 8u && variant >= 7u && variant <= 9u) + first = variant == 7u ? 0x4000u : variant == 8u ? 0x3ff0u : 0x3fe0u; + if (width == 4u && variant >= 9u) first = variant == 9u ? 0x3ff0u : 0x4000u; + std::array words; + for (uint32_t word = 0; word < width; ++word) { + MemoryInfo memory; + memory.kind = ResourceKind::ScalarAddress; + memory.offset = first + word * 4u; + if (variant == 3u && width == 8u && word == 7u) memory.offset += 4u; + words[word] = fixture->Emit(ValueOpcode::LoadAddressU32, + {address, Value(0u), Value(0u), Value(true)}, + fixture->AddMemory(memory, first == 0x3d0u ? 0x1110u : 0x1118u)); + } + return words; + }; + const auto image = fixture->Image(load(0x3d0u, 8u)); + const auto sampler_words = load(0x3f8u, 4u); + const auto sampler = fixture->Sampler( + {sampler_words[0], sampler_words[1], sampler_words[2], sampler_words[3]}); + MemoryInfo memory; + memory.kind = ResourceKind::Image; + memory.image_dimension = Decoder::ImageDimension::Dim2D; + fixture->Emit(ValueOpcode::ImageSampleRaw, + {image, sampler, fixture->ImageAddress(), variant == 5u ? active : Value(true)}, + fixture->AddMemory(memory, 0x1128u)); + return fixture; + }; + auto fixture = make(); + fixture->PlanAndTrack(); + const auto plan = ExtractResourcePlan(fixture->program); + Check(plan.info.images.size() == 1u && plan.info.samplers.size() == 1u, + "nested pointer descriptors were expanded before their runtime table was available"); + for (const auto source : {plan.info.images[0].source, plan.info.samplers[0].source}) { + const auto& descriptor = plan.descriptor_sources[source]; + const auto& indirect = descriptor.indirect_descriptor; + Check(indirect && !indirect->selector && indirect->pointer_table && + indirect->pointer_table->maximum == 65535u && indirect->table_stride == 592u && + indirect->pointer_table->offset == (descriptor.dword_count == 8u ? 0x3d0u : 0x3f8u), + "nested pointer descriptors lost their row geometry or captured the GPU selector payload"); + } + + constexpr uint32_t table = 0x2000u, objects = 0x4000u, stride = 592u, count = 6u; + LinearTestMemory memory; + memory.words.resize((objects + count * 0x430u - memory.base) / 4u); + memory.fail_address = 0x1800u; // The GPU selector's contents are unavailable to the CPU. + const auto at = [&](uint32_t address) { return memory.words.begin() + (address - memory.base) / 4u; }; + for (uint32_t row = 0; row < count; ++row) { + const auto object = objects + row * 0x430u; + *at(table + row * stride) = object; + const std::array image{10u + row, + uint32_t(Libs::Graphics::Prospero::BufferFormat::k32Float) << 20u, 0u, + Libs::Graphics::DstSel(4, 0, 0, 1) | (9u << 28u), 0u, 0u, 0u, 0u}; + std::copy(image.begin(), image.end(), at(object + 0x3d0u)); + const std::array sampler{row, 0x00fff000u, 0u, 0u}; + std::copy(sampler.begin(), sampler.end(), at(object + 0x3f8u)); + } + std::array user_data{table, stride << 16u, count, 0x5204u, + 0x1800u, 0u, 4u, 0x30005204u}; + const SrtRuntime runtime{.user_data = user_data, .userdata = &memory, + .read_specialization_memory = ReadLinearTestMemory}; + ResourceSnapshot snapshot; + ResourceSpecialization specialization; + Check(MaterializeResources(plan, runtime, snapshot, specialization) && + snapshot.images.size() == count + 1u && snapshot.samplers.size() == count + 1u, + "bounded nested pointer table lost image/sampler candidates or read the GPU selector"); + for (uint32_t row = 0; row < count; ++row) + Check(snapshot.images[row + 1u].dwords[0] == 10u + row && + snapshot.samplers[row + 1u].dwords[0] == row, + "nested pointer table mixed rows or pointee members"); + const auto populated = specialization; + const auto reads = memory.reads; + user_data[2] = 0u; + Check(MaterializeResources(plan, runtime, snapshot, specialization) && + snapshot.images.size() == 1u && snapshot.samplers.size() == 1u && + memory.reads == reads && specialization != populated, + "empty nested pointer table retained candidates or read an unavailable row"); + user_data[2] = count; + Check(MaterializeResources(plan, runtime, snapshot, specialization) && + specialization == populated, + "empty-to-positive pointer table refresh did not restore its specialization"); + *at(table) = objects + 0x430u; + *at(objects + 0x430u + 0x3d0u) = 42u; + *at(objects + 0x430u + 0x3f8u) = 7u; + Check(MaterializeResources(plan, runtime, snapshot, specialization) && + snapshot.images.size() == count && snapshot.samplers.size() == count && + snapshot.images[1].dwords[0] == 42u && snapshot.samplers[1].dwords[0] == 7u, + "nested descriptor refresh retained stale pointers or failed to deduplicate new pointees"); + *at(table) = objects; + + // A final low DWORD is in bounds; the high pointer DWORD is independently zero-filled. + user_data[1] = 1u << 16u; + user_data[2] = count * stride + 4u; + memory.watched_address = table + count * stride; + *at(static_cast(memory.watched_address)) = objects; + Check(MaterializeResources(plan, runtime, snapshot, specialization) && + memory.watched_reads != 0u && memory.watched_dwords == 1u && + snapshot.images.size() == count + 1u && snapshot.samplers.size() == count + 1u, + "partial pointer row was skipped or read beyond its scalar buffer bounds"); + const auto mapping = specialization.images[0].indirect_mapping_offset; + Check(snapshot.flattened_srt[mapping] == count + 1u && + snapshot.flattened_srt[mapping + count + 1u] == 1u, + "partial pointer row lost its low-word pointee or dense key mapping"); + user_data[1] = stride << 16u; + user_data[2] = count; + memory.fail_address = objects + 0x3d0u; + Check(!MaterializeResources(plan, runtime, snapshot, specialization), + "failed nested image pointee read was treated as a null descriptor"); + memory.fail_address = 0x1800u; + *at(table) = 0u; + Check(!MaterializeResources(plan, runtime, snapshot, specialization), + "unreadable null pointer row bypassed the strict pointee reader"); + *at(table) = objects; + + auto capped = make(4u); + capped->PlanAndTrack(); + const auto capped_plan = ExtractResourcePlan(capped->program); + user_data[2] = 65537u; + const auto before_cap = memory.reads; + Check(!MaterializeResources(capped_plan, runtime, snapshot, specialization) && + memory.reads == before_cap, + "oversized nested pointer table performed reads before enforcing its candidate cap"); + user_data[2] = count; + auto cyclic = make(6u); + cyclic->PlanAndTrack(); + Check(MaterializeResources(ExtractResourcePlan(cyclic->program), runtime, snapshot, specialization), + "context-free shift bound rejected a cyclic source or a bounded Select arm"); + auto boundary = make(9u); + boundary->PlanAndTrack(); // Both descriptor spans end exactly at the unmappable first-page boundary. + for (const uint32_t variant : {1u, 2u, 3u, 5u, 7u, 8u, 10u}) { + auto invalid = make(variant); + CheckFatal([&] { invalid->PlanAndTrack(); }, "not a valid runtime value", + "nested pointer planning accepted wrapping keys, mismatched words, lane-local bounds or mappable null-pointee bytes"); + } +} + void TestGuardedDirectImageTable() { namespace CFG = Libs::Graphics::ShaderRecompiler::CFG; enum class Guard { @@ -6248,6 +6443,7 @@ int main() { Run("unbounded material image keys", [] { TestInvariantIndirectImageMaterialization(false); }); Run("bounded image view eligibility", TestBoundedImageViewEligibility); Run("waterfall image table", TestWaterfallImageTable); + Run("nested pointer image table", TestNestedPointerImageTable); Run("guarded direct image table", TestGuardedDirectImageTable); Run("bounded compute image loop", TestBoundedComputeImageLoop); Run("uniformized material image keys", TestUniformizedMaterialImageKeys); diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index df8bd2c39..8eb09faa8 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -37549,6 +37549,115 @@ void CheckPointerImageTable(VulkanHarness &vulkan) { } } +void CheckNestedPointerImageTable(VulkanHarness &vulkan) { + using O = ShaderOpcode; + constexpr u32 keys_address = 128, table = 256, stride = 592, objects = 4096; + constexpr std::array keys{5u, 0u, 3u, 1u, 4u, 2u}; + for (const u32 wave : {32u, 64u}) { + TestCase test; + test.name = "NestedPointerImageSamplerTable"; + test.initial.resize((objects + keys.size() * 0x430u) / 4u, 0); + std::fill_n(test.initial.begin(), keys.size(), 0xdeadbeefu); + std::fill_n(test.initial.begin() + keys_address / 4u, keys.size(), UINT32_MAX); + for (u32 row = 0; row < keys.size(); ++row) { + const u32 object = objects + row * 0x430u; + test.initial[(table + row * stride) / 4u] = object; + auto image = MakeSampledTextureData(Prospero::BufferFormat::k32Float); + image[0] = 10u + row; + image[1] |= 1u << 30u; // Two texels: zero, then the row's value. + image[3] = DstSel(4, 0, 0, 1) | + (static_cast(Prospero::ImageType::kColor2D) << 28u); + std::copy_n(image.begin(), 8, test.initial.begin() + (object + 0x3d0u) / 4u); + const std::array sampler{0u, 0x00fff000u, (row & 1u) ? 0x0a500000u : 0u, 0u}; + std::copy(sampler.begin(), sampler.end(), + test.initial.begin() + (object + 0x3f8u) / 4u); + } + auto records = MakeStructuredStorageBufferData(stride, keys.size()); + records[0] = table; + std::copy_n(records.begin(), 4, test.user_data.begin() + 32); + auto key_buffer = MakeStructuredStorageBufferData(0, keys.size() * 4u); + key_buffer[0] = keys_address; + std::copy_n(key_buffer.begin(), 4, test.user_data.begin() + 4); + const auto output = MakeStructuredStorageBufferData(0, keys.size() * 4u); + std::copy_n(output.begin(), 4, test.user_data.begin() + 48); + test.bda_mappings = {{0u, 0u}}; + test.has_user_data = test.has_compute_info = true; + test.compute_info.wave_size = wave; + test.compute_info.threads_num[0] = test.compute_info.threads_num[1] = + test.compute_info.threads_num[2] = 1; + test.compute_info.group_id[0] = true; + test.compute_info.workgroup_register = 44; + test.dispatch_x = keys.size(); + auto &code = test.code; + code.push_back(EncodeSop2(0x1e, 23, 44, InlineU32(2))); + code.push_back(EncodeVop1(0x01, 30, 23)); + code.insert(code.end(), {EncodeMubuf0(0x0c), EncodeMubuf1(4, 1, 30), + EncodeVop2(0x16, 6, InlineU32(16), 4)}); + AppendVop3(&code, 0x360, 22, Vgpr(6), InlineU32(0)); + code.insert(code.end(), {EncodeSop2(0x26, 23, 22, 255), stride, + EncodeSmem0(0x09, 20, 16), EncodeSmem1(0, 23), + EncodeSmem0(0x03, 8, 10), EncodeSmem1(0x3d0, 125), + EncodeSmem0(0x02, 16, 10), EncodeSmem1(0x3f8, 125)}); + AppendVMovLiteral(&code, 20, std::bit_cast(0.5f)); + AppendVMovLiteral(&code, 21, std::bit_cast(0.5f)); + code.insert(code.end(), {EncodeMimg0(0x27, 1), EncodeMimg1(5, 20, 2, 4)}); + AppendBufferStoreDword(&code, 5, 30); + AppendEnd(&code); + 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::S_BUFFER_LOAD_DWORDX2, O::S_LOAD_DWORDX8, O::S_LOAD_DWORDX4, + O::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; + const auto compiled = CompileCase(test, vulkan.SubgroupSize()); + for (const auto &value : compiled.resources.buffers) { + ShaderBufferResource descriptor{}; + std::copy_n(value.dwords.begin(), 4, descriptor.fields); + test.storage_buffer_ranges.emplace_back(descriptor.Base48(), + std::max(4u, u32(descriptor.GetSize()))); + } + std::vector images; + for (const auto &descriptor : compiled.resources.images) + images.push_back(vulkan.CreateImage2D(test.name, 2, 1, vk::Format::eR32Sfloat, + vk::ImageUsageFlagBits::eSampled, + {0u, std::bit_cast(float(descriptor.dwords[0]))}, 1, + vk::ImageLayout::eShaderReadOnlyOptimal)); + std::vector samplers; + for (const auto &resource : compiled.program.info.samplers) { + ShaderSamplerResource descriptor{}; + std::copy_n(compiled.resources.samplers[resource.snapshot_index].dwords.begin(), + 4, descriptor.fields); + samplers.push_back(vulkan.CreateSampler(test.name, 1, + descriptor.XyMagFilter() == 1 ? vk::Filter::eLinear : vk::Filter::eNearest)); + } + TestCase producer; + producer.name = "NestedPointerSelectorProducer"; + 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] = 1; + producer.compute_info.wave_size = 32; + for (u32 group = 0; group < keys.size(); ++group) { + AppendVMovLiteral(&producer.code, 1, (keys[group] << 16u) | 0xbeefu); + AppendVMovLiteral(&producer.code, 2, keys_address + group * 4u); + AppendBufferStoreDword(&producer.code, 1, 2); + } + AppendEnd(&producer.code); + const auto producer_shader = CompileCase(producer, vulkan.SubgroupSize()); + auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size(), true); + vulkan.Dispatch(producer, producer_shader, buffer); + vulkan.Dispatch(test, compiled, buffer, nullptr, nullptr, nullptr, nullptr, + nullptr, images, samplers); + std::vector expected; + for (const auto key : keys) + expected.push_back(std::bit_cast(float(10u + key) * ((key & 1u) ? 0.5f : 1.0f))); + CompareWords(test, "GPU-selected nested image and sampler", expected, + vulkan.ReadBuffer(test.name, buffer, expected.size())); + vulkan.DestroyBuffer(&buffer); + for (const auto sampler : samplers) vulkan.Device().destroySampler(sampler); + for (auto &image : images) vulkan.DestroyImage(&image); + std::printf("[compute] %s wave=%u ok\n", test.name, wave); + } +} + void CheckGuardedRecordImageTable(VulkanHarness &vulkan) { using O = ShaderOpcode; constexpr u32 table = 256u; @@ -46364,6 +46473,7 @@ int main(int argc, char **argv) { VulkanHarness vulkan; CheckCountedRowImageSelector(vulkan); CheckPointerImageTable(vulkan); + CheckNestedPointerImageTable(vulkan); CheckGuardedRecordImageTable(vulkan); CheckIndirectImageKeySwitch(vulkan); CheckFiniteInlineSamplerPhi(vulkan); @@ -46536,6 +46646,7 @@ int main(int argc, char **argv) { CheckComputeThreadDimensions(vulkan); CheckCountedRowImageSelector(vulkan); CheckPointerImageTable(vulkan); + CheckNestedPointerImageTable(vulkan); CheckGuardedRecordImageTable(vulkan); CheckIndirectImageKeySwitch(vulkan); CheckDynamicSampleOffsets(vulkan, 32u);