diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index fd5ef57ed..2ccef1492 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -299,13 +299,13 @@ bool IsBoundedDescriptorTable(const ResourcePlan& program, template bool MaterializeIndirectDescriptor(const ResourcePlan& program, const DescriptorSource::IndirectDescriptor& indirect, - uint32_t resource_index, uint32_t dword_count, + uint32_t resource_index, uint32_t dword_count, uint32_t read_dwords, const SrtRuntime& runtime, SrtWalker& clean, ResourceSnapshot& snapshot, std::vector& descriptors, std::vector& specializations, uint32_t maximum_resources, Normalize&& normalize) { - const auto descriptor_bytes = dword_count * sizeof(uint32_t); + const auto descriptor_bytes = read_dwords * sizeof(uint32_t); const auto& sources = indirect.sources; const auto* selector = indirect.selector ? &*indirect.selector : nullptr; auto& keys = program.material_keys; @@ -445,12 +445,12 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p if (!ReadScalarTable(table_base, table_size, table_offset, runtime, address) || !ReadScalarTable((uint64_t {address[1]} << 32u) | address[0], UINT64_MAX, *indirect.pointer_offset, runtime, - std::span(candidate.dwords).first(dword_count))) return false; + std::span(candidate.dwords).first(read_dwords))) return false; } else if (affine) { if (!ReadScalarTable(table_base, table_size, table_offset, runtime, - std::span(candidate.dwords).first(dword_count))) return false; + std::span(candidate.dwords).first(read_dwords))) return false; } else { - std::copy_n(keys.begin() + word, std::min(dword_count, keys.size() - word), + std::copy_n(keys.begin() + word, std::min(read_dwords, keys.size() - word), candidate.dwords.begin()); } if (!normalize(candidate)) @@ -672,7 +672,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p table_offset = uint64_t {(key * indirect.table_stride + indirect.table_offset) & ~3u} + (indirect.table_immediate & ~3u); if (!ReadScalarTable(table_base, table_size, table_offset, runtime, - std::span(candidate.dwords).first(dword_count))) { + std::span(candidate.dwords).first(read_dwords))) { return false; } } else if (!clean.EvaluateDescriptor(sources[entry], candidate)) { @@ -1441,7 +1441,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime snapshot.buffers[i] = {.dword_count = 4u}; if (active.empty() || active[base.source]) { if (!MaterializeIndirectDescriptor( - program, *source->indirect_descriptor, i, 4u, observed, clean, snapshot, + program, *source->indirect_descriptor, i, 4u, 4u, observed, clean, snapshot, snapshot.buffers, specialization.buffers, UINT32_MAX, [&](DescriptorValue& value) { return NormalizeIndirectBuffer( @@ -1505,7 +1505,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime const auto& indirect = *source->indirect_descriptor; const bool bounded_table = IsBoundedDescriptorTable(program, indirect); if (!MaterializeIndirectDescriptor( - program, indirect, i, 8u, observed, clean, snapshot, snapshot.images, + program, indirect, i, 8u, image.r128 ? 4u : 8u, observed, clean, snapshot, snapshot.images, specialization.images, UINT32_MAX, [&](DescriptorValue& value) { // A broad heap also contains resources for other typed image operations. if (NullImageDescriptor(value) || !ValidImageDescriptor(value, image.r128) || @@ -1538,7 +1538,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime 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, + program, *indirect, i, 4u, 4u, observed, clean, snapshot, snapshot.samplers, specialization.samplers, ShaderInfo::MaxSamplers, [](DescriptorValue&) { return true; })) return false; } else { diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp index 0ee81191c..88e93963a 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp @@ -513,7 +513,7 @@ public: } } if (plan.reads[0] != nullptr && !plan.retain_reads) { - for (uint32_t word = 0; word < plan.handle->NumArgs(); ++word) + for (uint32_t word = 0; word < plan.read_dwords; ++word) m_program.memory_info[plan.memory[word]].planning_only = true; } } @@ -522,7 +522,7 @@ public: for (const auto& plan: m_indirect_descriptors) { if (plan.handle->GetOpcode() != ValueOpcode::GetImageResource || plan.reads[0] == nullptr) continue; - for (const auto* read: plan.reads) { + for (const auto* read: std::span(plan.reads).first(plan.read_dwords)) { const auto first = std::ranges::find_if(m_indirect_descriptors, [&](const auto& candidate) { return candidate.reads[0] == read; }); @@ -559,6 +559,7 @@ private: struct IndirectDescriptorPlan { Inst* handle = nullptr; uint32_t source = 0; + uint32_t read_dwords = 0; Value key; std::array roots {}; std::array memory {}; @@ -2196,7 +2197,8 @@ private: } bool MatchScalarMaterialKey(Value key, DescriptorSource::IndirectDescriptor& indirect, - DescriptorSource& material_source, bool require_bound) { + DescriptorSource& material_source, bool require_bound, + bool allow_scalar_domain = false) { DescriptorSource::IndirectDescriptor::SelectorRead selector; const auto* read = SelectorReadValue(key, Value(true), selector); if (read == nullptr) return false; @@ -2211,9 +2213,10 @@ private: uint32_t offset = 0, stride = 0; if (memory == nullptr || memory->offset > INT32_MAX || !MemoryIndexBelongsTo(memory_index, *read)) return false; - if (MatchTableOffset(read->Arg(1), index, offset, stride) && + const bool affine = MatchTableOffset(read->Arg(1), index, offset, stride) && ((offset | stride | memory->offset) & 3u) == 0u && - uint64_t {offset} + memory->offset <= UINT32_MAX) { + uint64_t {offset} + memory->offset <= UINT32_MAX; + if (affine) { workgroup_axis = WorkgroupAxis(index); if (workgroup_axis == UINT32_MAX) { const auto* sum = index.Resolve().TryInstruction(); @@ -2242,6 +2245,12 @@ private: if (rows) { stride = 4u; offset = 0u; + } else if (count.IsEmpty() && workgroup_axis == UINT32_MAX && + !require_bound && allow_scalar_domain && affine && stride != 0u) { + // The bounded selector V# covers this complete U32 address congruence + // class, including the zero value of out-of-bounds scalar reads. + stride = 1u << std::countr_zero(stride); + offset %= stride; } else if (count.IsEmpty() && workgroup_axis == UINT32_MAX) { auto address = read->Arg(1); auto active = Value(true); @@ -2666,7 +2675,7 @@ private: uint32_t& table_stride, uint32_t& table_immediate) { Inst* table_handle = nullptr; Value table_guard; - for (uint32_t dword = 0; dword < handle.NumArgs(); ++dword) { + for (uint32_t dword = 0; dword < plan.read_dwords; ++dword) { auto* read = descriptor.dwords[dword].Resolve().TryInstruction(); if (read == nullptr) { return false; @@ -2734,9 +2743,9 @@ private: } plan.split_offsets |= !indexed && offset != table_offset; table_handle = current_handle; - if (handle.NumArgs() == 8u) { - if (memory->kind == ResourceKind::ScalarAddress) - plan.retain_reads |= !UsesOnlyImageDescriptors(*read); + if (handle.GetOpcode() != ValueOpcode::GetBufferResource) { + plan.retain_reads |= handle.GetOpcode() == ValueOpcode::GetSamplerResource || + (memory->kind == ResourceKind::ScalarAddress && !UsesOnlyImageDescriptors(*read)); } else if (std::ranges::any_of(read->Uses(), [&](const Use& use) { return use.user->GetOpcode() != ValueOpcode::GetBufferResource || std::ranges::any_of(use.user->Uses(), [&](const Use& consumer) { @@ -2770,7 +2779,7 @@ private: handle.GetOpcode() != ValueOpcode::GetSamplerResource) return false; Inst* pointer = nullptr; uint32_t descriptor_offset = 0; - for (uint32_t word = 0; word < handle.NumArgs(); ++word) { + for (uint32_t word = 0; word < plan.read_dwords; ++word) { const auto* read = descriptor.dwords[word].Resolve().TryInstruction(); uint32_t index, offset; const auto* memory = read != nullptr ? ScalarReadMemory(*read, index) : nullptr; @@ -2789,7 +2798,7 @@ private: } // 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) + if (pointer == nullptr || uint64_t {descriptor_offset} + plan.read_dwords * 4u > 0x4000u) return false; const auto* low = pointer->Arg(0).Resolve().TryInstruction(); const auto* high = pointer->Arg(1).Resolve().TryInstruction(); @@ -2830,9 +2839,11 @@ private: return true; } - bool TryMakeIndirectImage(Inst& handle, const DescriptorSource& descriptor, - IndirectDescriptorPlan& plan) { - if (handle.GetOpcode() != ValueOpcode::GetImageResource || handle.NumArgs() != 8u) return false; + bool TryMakeTableDescriptor(Inst& handle, const DescriptorSource& descriptor, + IndirectDescriptorPlan& plan) { + const bool sampler = handle.GetOpcode() == ValueOpcode::GetSamplerResource; + if ((!sampler && handle.GetOpcode() != ValueOpcode::GetImageResource) || + handle.NumArgs() != (sampler ? 4u : 8u)) return false; DescriptorSource table_source; Value key; uint32_t table_offset = 0; @@ -2852,7 +2863,7 @@ private: plan.retain_reads = true; } else if (!plan.table_indexed && MatchScalarMaterialKey(key, indirect, material_source, - table_source.dword_count == 4u)) { + !sampler && table_source.dword_count == 4u, sampler)) { // Keep live descriptor payloads, but project descriptor-only SMEM onto // the GPU key using the same plan as other indirect image reads. plan.retain_reads |= table_source.dword_count == 4u; @@ -2862,11 +2873,11 @@ private: // A runtime count can describe fewer rows than the mask's static bit range. if (guarded_count.IsEmpty()) guarded_count = UnsignedBoundOnEntry(key, handle.Parent(), true); if (!guarded_count.IsEmpty()) { - if (table_stride < 32u || (table_stride & 3u) != 0u) return false; + if (table_stride < plan.read_dwords * 4u || (table_stride & 3u) != 0u) return false; // Materialization captures the count and descriptor reads; renderer alias // checks reject writes to those ranges while unrelated stores remain valid. indirect.key_count = guarded_count; - } else if (table_stride != 32u) { + } else if (table_stride != plan.read_dwords * 4u) { return false; } else { uint32_t first = 0; @@ -2911,31 +2922,31 @@ private: } else { // A bounded V# supplies the complete image table. Leave every GPU selector // and descriptor read in the shader; the host only translates table bytes. - if (plan.split_offsets) return false; + if (sampler || plan.split_offsets) return false; indirect.table_stride = 0u; indirect.table_offset = 0u; indirect.table_immediate = 0u; indirect.workgroup_axis = UINT32_MAX; plan.table_read = plan.reads[0]; - if (plan.table_indexed) indirect.table_record_bytes = table_immediate + 32u; + if (plan.table_indexed) indirect.table_record_bytes = table_immediate + plan.read_dwords * 4u; else indirect.table_scalar = true; plan.retain_reads = true; } if (plan.retain_reads) plan.reads.fill(nullptr); indirect.table_source = InternSource(table_source); - DescriptorSource image_source; - image_source.dword_count = 8u; - image_source.dwords.fill(Value(0u)); + DescriptorSource source; + source.dword_count = handle.NumArgs(); + 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_descriptor = indirect; + source.dwords.begin()); + if (!sampler) std::copy_n(table_source.dwords.begin(), table_source.dword_count, + source.dwords.begin() + 4u); + source.indirect_descriptor = indirect; plan.handle = &handle; - plan.source = InternSource(image_source); + plan.source = InternSource(source); plan.key = key; - plan.roots = image_source.dwords; + plan.roots = source.dwords; return true; } @@ -3375,8 +3386,8 @@ private: return std::any_of(m_indirect_descriptors.begin(), m_indirect_descriptors.end(), [&](const IndirectDescriptorPlan& plan) { return plan.reads[0] != nullptr && !plan.retain_reads && - std::find(plan.memory.begin(), plan.memory.begin() + plan.handle->NumArgs(), index) != - plan.memory.begin() + plan.handle->NumArgs(); + std::find(plan.memory.begin(), plan.memory.begin() + plan.read_dwords, index) != + plan.memory.begin() + plan.read_dwords; }); } @@ -3398,11 +3409,23 @@ private: uint32_t bad_dword = 0; if (ValidateSource(descriptor, bad_dword)) return; IndirectDescriptorPlan plan; + plan.read_dwords = width; + if (!sampler && width == 8u && memory.image_r128) { + // R128 handles are normalized to eight words; only their low four + // words come from memory. Every use must agree on that encoding. + if (!std::ranges::all_of(std::span(descriptor.dwords).subspan(4u), + [](Value word) { return word.Resolve() == Value(0u); }) || + !std::ranges::all_of(handle->Uses(), [&](const Use& use) { + return ImageOpcodeInfoOf(use.user->GetOpcode()).access != ImageAccess::None && + m_program.memory_info[use.user->Flags().index].image_r128; + })) return; + plan.read_dwords = 4u; + } if (TryMakeNestedDescriptor(*handle, descriptor, plan) || - (sampler ? TryMakeFiniteDescriptor(*handle, plan) - : (TryMakeIndirectImage(*handle, descriptor, plan) || - TryMakeFiniteDescriptor(*handle, plan) || - TryMakeIndirectBuffer(*handle, descriptor, plan)))) { + (sampler && TryMakeFiniteDescriptor(*handle, plan)) || + TryMakeTableDescriptor(*handle, descriptor, plan) || + (!sampler && (TryMakeFiniteDescriptor(*handle, 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)) diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index 7d15a00b2..d3656f93d 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -2933,6 +2933,143 @@ void TestUniformScalarBufferImage() { "unavailable nested image table was accepted"); } +void TestCompactScalarImageTable() { + using F = Libs::Graphics::Prospero::BufferFormat; + constexpr uint32_t stride = 0x330u, image_offset = 0x98u, sampler_offset = 0x88u; + const auto make_plan = [&](bool compact, uint32_t invalid = 0u) { + Fixture fixture; + const auto list = invalid == 1u + ? fixture.Address(fixture.UserData(0), fixture.UserData(1)) + : fixture.Buffer({fixture.UserData(0), fixture.UserData(1), + fixture.UserData(2), fixture.UserData(3)}); + const auto table = fixture.Buffer({fixture.UserData(4), fixture.UserData(5), + fixture.UserData(6), fixture.UserData(7)}); + // Uniform on the GPU, but no CPU-evaluable row or workgroup-prefix proof. + const auto group = fixture.Emit(ValueOpcode::GetBuiltin, + {Value(static_cast(StageInputKind::WorkgroupId)), Value(0u)}); + const auto index = fixture.Emit(ValueOpcode::BitwiseXor32, {group, Value(1u)}); + const auto offset = fixture.Emit(invalid == 2u ? ValueOpcode::UDiv32 + : ValueOpcode::ShiftLeftLogical32, + {index, Value(2u)}); + MemoryInfo scalar; + scalar.kind = invalid == 1u ? ResourceKind::ScalarAddress : ResourceKind::ScalarBuffer; + const auto key = invalid == 1u + ? fixture.Emit(ValueOpcode::LoadAddressU32, {list, offset, Value(0u), Value(true)}, + fixture.AddMemory(scalar, 0x570u)) + : fixture.Emit(ValueOpcode::ReadConstBuffer, {list, offset}, + fixture.AddMemory(scalar, 0x570u)); + const auto record = fixture.Emit(ValueOpcode::IMul32, {key, Value(stride)}); + scalar.kind = ResourceKind::ScalarBuffer; + std::array image_words; + std::array sampler_words; + for (uint32_t word = 0; word < 8u; ++word) { + scalar.offset = image_offset + word * 4u; + image_words[word] = compact && word >= 4u ? Value(0u) + : fixture.Emit(ValueOpcode::ReadConstBuffer, {table, record}, + fixture.AddMemory(scalar, 0x590u)); + if (word < 4u) { + scalar.offset = sampler_offset + word * 4u; + sampler_words[word] = fixture.Emit(ValueOpcode::ReadConstBuffer, {table, record}, + fixture.AddMemory(scalar, 0x590u)); + } + } + if (invalid == 3u) image_words[7] = Value(1u); + const auto image = fixture.Image(image_words, 0x59cu); + MemoryInfo sample; + sample.kind = ResourceKind::Image; + sample.image_dimension = Decoder::ImageDimension::Dim2D; + sample.image_r128 = compact; + const auto sampler = fixture.Sampler(sampler_words, 0x59cu); + fixture.Emit(ValueOpcode::ImageSampleRaw, + {image, sampler, fixture.ImageAddress(), Value(true)}, fixture.AddMemory(sample, 0x59cu)); + if (invalid == 4u) { + sample.image_r128 = false; + fixture.Emit(ValueOpcode::ImageSampleRaw, + {image, sampler, fixture.ImageAddress(), Value(true)}, fixture.AddMemory(sample, 0x5a0u)); + } + fixture.PlanAndTrack(); + const auto &source = fixture.program.descriptor_sources[fixture.program.info.samplers[0].source]; + Check(source.indirect_descriptor && source.indirect_descriptor->selector && + source.indirect_descriptor->selector->stride == 4u && + source.indirect_descriptor->selector_first.IsEmpty() && + source.indirect_descriptor->key_count.IsEmpty() && + source.indirect_descriptor->table_stride == stride && + source.indirect_descriptor->table_immediate == sampler_offset, + "compact image sampler lost its complete bounded index-list domain"); + return ExtractResourcePlan(fixture.program); + }; + for (const bool compact : {false, true}) { + auto plan = make_plan(compact); + const uint32_t width = compact ? 4u : 8u; + const auto bytes = 2u * stride + image_offset + width * 4u; + std::array data{0x1000u, 0u, 8u, 0u, 0x2000u, 0u, bytes, 0u}; + struct Memory : LinearTestMemory { uint32_t selector_attempts = 0; } memory; + memory.words.resize((0x1000u + bytes) / 4u, 0xdeadbeefu); + memory.words[0] = 2u; + memory.words[1] = 1u; + for (uint32_t row = 0; row < 3u; ++row) { + auto at = memory.words.begin() + (0x1000u + row * stride + sampler_offset) / 4u; + std::fill_n(at, 4u, 0u); + at[0] = row; + at += 4; + std::fill_n(at, width, 0u); + at[0] = 0x100u + row; + at[1] = static_cast(F::k32_32_32_32Float) << 20u; + at[3] = Libs::Graphics::DstSel(4, 5, 6, 7) | + (static_cast(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u); + if (!compact) at[5] = 0x00700000u; + } + memory.fail_address = 0x2000u + bytes; // Last descriptor ends at the readable boundary. + SrtRuntime runtime{.user_data = data, .userdata = &memory, + .read_specialization_memory = [](void *userdata, uint64_t address, + std::span words) { + auto &memory = *static_cast(userdata); + if (address == memory.base) ++memory.selector_attempts; + return ReadLinearTestMemory(&memory, address, words); + }}; + ResourceSnapshot snapshot; + ResourceSpecialization specialization; + const auto materialize = [&] { + Check(MaterializeResources(plan, runtime, snapshot, specialization), + "paired compact image/sampler table did not materialize"); + Check(snapshot.images.size() == 4u && snapshot.samplers.size() == 3u, + "paired table omitted an image, index-list value or OOB zero key"); + for (const auto &image : snapshot.images) { + Check(image.dword_count == 8u, "compact image escaped normalized eight-DWORD ABI"); + if (compact) Check(std::ranges::all_of(std::span(image.dwords).subspan(4u), + [](uint32_t word) { return word == 0u; }), + "R128 image copied poisoned adjacent record fields"); + } + }; + materialize(); + const auto original = specialization; + memory.words[(0x1000u + stride + image_offset) / 4u] = 0x201u; + materialize(); + Check(specialization == original && std::ranges::any_of(snapshot.images, + [](const auto &image) { return image.dwords[0] == 0x201u; }), + "compact descriptor payload refresh changed specialization or stayed stale"); + data[2] = 4u; + Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.samplers.size() == 2u, + "index-list count refresh retained a stale sampler candidate"); + data[2] = 0u; + Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.samplers.size() == 1u && + snapshot.samplers[0].dwords[0] == 0u, + "empty index list lost the scalar OOB zero key"); + data[2] = 8u; + memory.fail_address = 0x1004u; + Check(!MaterializeResources(plan, runtime, snapshot, specialization), + "unreadable selected index-list bytes were accepted"); + memory.fail_address = UINT64_MAX; + data[2] = (MaxIndirectDescriptorProbes + 1u) * 4u; + memory.selector_attempts = 0u; + Check(!MaterializeResources(plan, runtime, snapshot, specialization) && memory.selector_attempts == 0u, + "oversized index list was read before the existing probe cap"); + } + for (uint32_t invalid : {1u, 2u, 3u, 4u}) + CheckFatal([&] { make_plan(true, invalid); }, "not a valid runtime value", + "compact table accepted an unproved selector or inconsistent descriptor width"); +} + void TestScalarBufferSelectorDomain() { const auto make_plan = [](bool active = true, bool sparse = false, bool uniformize_first = false) { Fixture fixture; @@ -7313,6 +7450,7 @@ int main() { Run("uniformized material image keys", TestUniformizedMaterialImageKeys); Run("image descriptor fields", TestImageDescriptorFields); Run("draw-uniform scalar image", TestUniformScalarBufferImage); + Run("compact scalar image table", TestCompactScalarImageTable); Run("scalar buffer selector domain", TestScalarBufferSelectorDomain); Run("private LDS forwarding", TestPrivateLdsForwarding); Run("counted row image selector", TestCountedRowImageSelector); diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index d8995be9c..526267a94 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -38696,33 +38696,46 @@ void CheckPointerImageTable(VulkanHarness &vulkan) { void CheckNestedPointerImageTable(VulkanHarness &vulkan) { using O = ShaderOpcode; - constexpr u32 keys_address = 128, table = 256, stride = 592, objects = 4096; + constexpr u32 keys_address = 128, list_address = 160, table = 256, objects = 4096; constexpr std::array keys{5u, 0u, 3u, 1u, 4u, 2u}; + constexpr std::array list{2u, 1u, 4u, 0u, 5u, 3u}; + for (const bool compact : {false, true}) for (const u32 wave : {32u, 64u}) { + const u32 stride = compact ? 0x330u : 592u; + const u32 table_bytes = (keys.size() - 1u) * stride + 0x98u + 16u; TestCase test; - test.name = "NestedPointerImageSamplerTable"; - test.initial.resize((objects + keys.size() * 0x430u) / 4u, 0); + test.name = compact ? "CompactRecordImageSamplerTable" : "NestedPointerImageSamplerTable"; + test.initial.resize((compact ? table + table_bytes : objects + keys.size() * 0x430u) / 4u, + compact ? 0xdeadbeefu : 0u); std::fill_n(test.initial.begin(), keys.size(), 0xdeadbeefu); std::fill_n(test.initial.begin() + keys_address / 4u, keys.size(), UINT32_MAX); + if (compact) std::copy(list.begin(), list.end(), test.initial.begin() + list_address / 4u); for (u32 row = 0; row < keys.size(); ++row) { - const u32 object = objects + row * 0x430u; - test.initial[(table + row * stride) / 4u] = object; + const u32 object = compact ? table + row * stride : objects + row * 0x430u; + if (!compact) 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); + std::copy_n(image.begin(), compact ? 4u : 8u, + test.initial.begin() + (object + (compact ? 0x98u : 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); + test.initial.begin() + (object + (compact ? 0x88u : 0x3f8u)) / 4u); } - auto records = MakeStructuredStorageBufferData(stride, keys.size()); + auto records = MakeStructuredStorageBufferData(compact ? 0u : stride, + compact ? table_bytes : 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); + if (compact) { + auto indices = MakeStructuredStorageBufferData(0, list.size() * 4u); + indices[0] = list_address; + std::copy_n(indices.begin(), 4, test.user_data.begin() + 36); + } const auto output = MakeStructuredStorageBufferData(0, keys.size() * 4u); std::copy_n(output.begin(), 4, test.user_data.begin() + 48); test.bda_mappings = {{0u, 0u}}; @@ -38739,20 +38752,39 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) { 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, + if (compact) { + code.insert(code.end(), {EncodeSop2(0x1e, 23, 22, InlineU32(2)), + EncodeSmem0(0x08, 22, 18), EncodeSmem1(0, 23), + EncodeSop2(0x26, 23, 22, 255), stride, + EncodeSmem0(0x0b, 8, 16), EncodeSmem1(0x88u, 23)}); + for (u32 reg = 16u; reg < 20u; ++reg) AppendSMovLiteral(&code, reg, 0xdeadbeefu); + } else 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)}); + code.insert(code.end(), {EncodeMimg0(0x27, 1, 0, false, 1, compact), + EncodeMimg1(5, 20, compact ? 3u : 2u, compact ? 2u : 4u)}); 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}; + if (compact) { + test.opcodes.insert(test.opcodes.end(), {O::S_BUFFER_LOAD_DWORD, + O::S_BUFFER_LOAD_DWORDX8, O::S_MOV_B32}); + test.decoded_counts = {{"r128=1", 1}}; + } else test.opcodes.insert(test.opcodes.end(), + {O::S_BUFFER_LOAD_DWORDX2, O::S_LOAD_DWORDX8, O::S_LOAD_DWORDX4}); const auto compiled = CompileCase(test, vulkan.SubgroupSize()); + if (compact) Require(test.name, "physical R128 descriptor boundary", + compiled.resources.images.size() == keys.size() + 1u && + std::ranges::all_of(compiled.resources.images, [](const auto &image) { + return image.dword_count == 8u && + std::ranges::all_of(std::span(image.dwords).subspan(4u), + [](u32 word) { return word == 0u; }); + }), "compact descriptor copied adjacent poison or omitted the trailing 16-byte image"); for (const auto &value : compiled.resources.buffers) { ShaderBufferResource descriptor{}; std::copy_n(value.dwords.begin(), 4, descriptor.fields); @@ -38792,8 +38824,10 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) { vulkan.Dispatch(test, compiled, buffer, nullptr, nullptr, nullptr, nullptr, nullptr, images, samplers); std::vector expected; - for (const auto key : keys) + for (auto key : keys) { + if (compact) key = list[key]; 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);