diff --git a/src/graphics/presentation/window/vulkanWindow.cpp b/src/graphics/presentation/window/vulkanWindow.cpp index 453013441..0ddf31621 100644 --- a/src/graphics/presentation/window/vulkanWindow.cpp +++ b/src/graphics/presentation/window/vulkanWindow.cpp @@ -55,6 +55,7 @@ vk::PhysicalDeviceVulkan12Features WindowContext::RequiredVulkan12Features() noe features.shaderOutputViewportIndex = VK_TRUE; features.bufferDeviceAddress = VK_TRUE; features.shaderBufferInt64Atomics = VK_TRUE; + features.shaderSampledImageArrayNonUniformIndexing = VK_TRUE; return features; } @@ -266,6 +267,9 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa required_features12.bufferDeviceAddress); check_feature(features12.shaderBufferInt64Atomics, "shaderBufferInt64Atomics", required_features12.shaderBufferInt64Atomics); + check_feature(features12.shaderSampledImageArrayNonUniformIndexing, + "shaderSampledImageArrayNonUniformIndexing", + required_features12.shaderSampledImageArrayNonUniformIndexing); check_feature(features13.robustImageAccess, "robustImageAccess"); check_feature(features13.dynamicRendering, "dynamicRendering", required_features13.dynamicRendering); diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAnalysis.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAnalysis.cpp index e645ed2e9..604b1ea86 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAnalysis.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAnalysis.cpp @@ -83,7 +83,7 @@ uint32_t DescriptorElementPointer(EmitterState& state, uint32_t result_ptr_type, } const auto pointer = state.builder.AllocateId(); state.builder.AddFunction(spv::OpAccessChain, result_ptr_type, pointer, variable_id, - ConstantU32(state, array_index)); + array_index); return pointer; } @@ -152,8 +152,9 @@ uint32_t ImageViewSizeType(EmitterState& state, ImageDimension dimension) { } } -uint32_t LoadImageDescriptor(EmitterState& state, uint32_t resource, uint32_t mip) { - const auto pointer = ImageDescriptorPointer(state, resource, mip); +uint32_t LoadImageDescriptor(EmitterState& state, uint32_t resource, uint32_t mip, + uint32_t array_index) { + const auto pointer = ImageDescriptorPointer(state, resource, mip, array_index); const auto image = state.builder.AllocateId(); state.builder.AddFunction(spv::OpLoad, ImageType(state, state.program.info.images.at(resource)), image, pointer); @@ -167,30 +168,40 @@ uint32_t LoadSamplerDescriptor(EmitterState& state, uint32_t sampler) { const auto pointer_type = state.builder.Type(spv::OpTypePointer, spv::StorageClassUniformConstant, sampler_type); const auto pointer = DescriptorElementPointer( - state, pointer_type, state.sampler_variable, array_index, + state, pointer_type, state.sampler_variable, ConstantU32(state, array_index), IR::DescriptorBindingKind::Samplers, sampler, "sampler descriptor array was not emitted"); const auto sampler_id = state.builder.AllocateId(); state.builder.AddFunction(spv::OpLoad, sampler_type, sampler_id, pointer); return sampler_id; } -uint32_t MakeSampledImage(EmitterState& state, uint32_t resource, uint32_t sampler_id, uint32_t mip) { +uint32_t MakeSampledImage(EmitterState& state, uint32_t resource, uint32_t sampler_id, uint32_t mip, + uint32_t array_index) { const auto& image_resource = state.program.info.images.at(resource); EXIT_IF(image_resource.resource_class != IR::ImageResourceClass::Sampled); - const auto image = LoadImageDescriptor(state, resource, mip); + const auto image = LoadImageDescriptor(state, resource, mip, array_index); const auto sampled_image = state.builder.AllocateId(); const auto sampled_type = state.builder.Type(spv::OpTypeSampledImage, ImageType(state, image_resource)); state.builder.AddFunction(spv::OpSampledImage, sampled_type, sampled_image, image, sampler_id); + if (array_index != 0u) { + state.builder.RequireExtension("SPV_EXT_descriptor_indexing"); + state.builder.RequireCapability(spv::CapabilityShaderNonUniform); + state.builder.RequireCapability(spv::CapabilitySampledImageArrayNonUniformIndexing); + state.builder.AddAnnotation(spv::OpDecorate, sampled_image, spv::DecorationNonUniform); + } return sampled_image; } -uint32_t ImageDescriptorPointer(EmitterState& state, uint32_t resource, uint32_t mip) { +uint32_t ImageDescriptorPointer(EmitterState& state, uint32_t resource, uint32_t mip, + uint32_t array_index) { const auto& image = state.program.info.images.at(resource); EXIT_IF(mip >= image.mip_count); const auto kind = IR::DescriptorBindingForImage(image); EXIT_IF(!kind.has_value()); - const auto array_index = ResourceForDescriptor(state, *kind, resource) + mip; + if (array_index == 0u) { + array_index = ConstantU32(state, ResourceForDescriptor(state, *kind, resource) + mip); + } const auto pointer_type = state.builder.Type( spv::OpTypePointer, spv::StorageClassUniformConstant, ImageType(state, image)); const auto variable = state.image_variables[IR::ImageBindingIndex(*kind)]; diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp index c51b23618..b0a78f834 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp @@ -776,13 +776,13 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) { operands.push_back(AddressF32(ctx, mem, *address, layout.bias)); } const auto sampler_id = LoadSamplerDescriptor(state, mem.sampler); - const auto EmitSample = [&](uint32_t resource) { + const auto EmitSample = [&](uint32_t resource, uint32_t array_index = 0u) { const auto& candidate = state.program.info.images[resource]; const auto coord = CoordF32(ctx, mem, *address, layout.coord, ImageDimensionInfoFor(candidate.dimension).coordinate_components, candidate.cube); - const auto sampled = MakeSampledImage(state, resource, sampler_id); + const auto sampled = MakeSampledImage(state, resource, sampler_id, 0u, array_index); const auto sample = state.builder.AllocateId(); std::vector sample_operands {result_type, sample, sampled, coord}; if (dref) { @@ -821,6 +821,41 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) { } const auto selected = EmitIndirectResourceIndex( state, key, image.indirect_mapping_offset, image.indirect_search_iterations, 0u); + const auto kind = *IR::DescriptorBindingForImage(image); + const auto& binding = *IR::FindBinding(state.program.bindings, kind); + uint32_t first_child = 0; + bool homogeneous = true; + for (uint32_t ordinal = 0; ordinal < image.indirect_resources.size(); ++ordinal) { + const auto resource = image.indirect_resources[ordinal]; + const auto& candidate = state.program.info.images[resource]; + if (candidate.dimension != image.dimension || candidate.cube != image.cube || + IR::DescriptorBindingForImage(candidate) != kind || + candidate.mip_count != 1u) { + homogeneous = false; + break; + } + if (ordinal == 1u) first_child = ResourceForDescriptor(state, kind, resource); + const auto child_slot = first_child + ordinal - 1u; + if (ordinal > 1u && (child_slot >= binding.resources.size() || + binding.resources[child_slot] != resource)) { + homogeneous = false; + break; + } + } + if (homogeneous) { + const auto child_index = Binary(state, spv::OpIAdd, TypeU32(state), selected, + ConstantU32(state, first_child - 1u)); + const auto is_root = Binary(state, spv::OpIEqual, TypeBool(state), selected, + ConstantU32(state, 0u)); + const auto array_index = state.builder.AllocateId(); + state.builder.AddFunction(spv::OpSelect, TypeU32(state), array_index, is_root, + ConstantU32(state, ResourceForDescriptor(state, kind, mem.resource)), + child_index); + const auto sample = EmitSample(mem.resource, array_index); + const auto result = dref ? sample : UnpackImageTexel(ctx, mem, sample); + ctx.Define(inst, ResultVector(ctx, result, numeric_class, dref, mem)); + return; + } const auto default_label = state.builder.AllocateId(); const auto merge_label = state.builder.AllocateId(); std::vector labels(image.indirect_resources.size() - 1u); diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h index 74fc754ea..37a758755 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h @@ -316,14 +316,16 @@ uint32_t ImageType(EmitterState& state, const IR::ImageResource& image); uint32_t ImageViewSizeType(EmitterState& state, ImageDimension dimension); -uint32_t ImageDescriptorPointer(EmitterState& state, uint32_t resource, uint32_t mip = 0); +uint32_t ImageDescriptorPointer(EmitterState& state, uint32_t resource, uint32_t mip = 0, + uint32_t array_index = 0); -uint32_t LoadImageDescriptor(EmitterState& state, uint32_t resource, uint32_t mip = 0); +uint32_t LoadImageDescriptor(EmitterState& state, uint32_t resource, uint32_t mip = 0, + uint32_t array_index = 0); uint32_t LoadSamplerDescriptor(EmitterState& state, uint32_t sampler); uint32_t MakeSampledImage(EmitterState& state, uint32_t resource, uint32_t sampler_id, - uint32_t mip = 0); + uint32_t mip = 0, uint32_t array_index = 0); void EmitStorageImageWrite(EmitterState& state, uint32_t resource, uint32_t mip_lod, uint32_t coord, uint32_t texel); diff --git a/src/graphics/shader/recompiler/ir/ShaderIR.h b/src/graphics/shader/recompiler/ir/ShaderIR.h index 8c8d817ec..a3427c09e 100644 --- a/src/graphics/shader/recompiler/ir/ShaderIR.h +++ b/src/graphics/shader/recompiler/ir/ShaderIR.h @@ -487,6 +487,7 @@ struct DescriptorSource { uint32_t selector_stride = 0; uint32_t selector_offset = 0; uint32_t table_offset = 0; + uint32_t selector_shift = 0; Value key_count; Value selector_first; Value selector_mask; diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index c63434529..3f37c5f55 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -285,6 +285,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p ShaderBufferResource material; uint32_t first = 0, count = 0; if (!DecodeBufferDescriptor(material_value, material) || material.Type() != 0u || + (indirect.selector_shift != 0u && (material.Base48() & 3u) != 0u) || material.SwizzleEnabled() || material.AddTid() || material.OutOfBounds() != 0u || uint64_t {indirect.selector_offset} + 4u > material.Stride() || @@ -296,17 +297,20 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p indirect.selector_offset + 4u > uint64_t {UINT32_MAX} + 1u) return false; - keys.reserve(count); - for (uint64_t index = first; index < uint64_t {first} + count; ++index) { - uint32_t key = 0; - const auto offset = index * material.Stride() + indirect.selector_offset; - if (!ReadScalarTable(material.Base48(), material.GetSize(), offset, runtime, - {&key, 1})) - return false; - keys.push_back(key); + if (material.Stride() == 4u && indirect.selector_offset == 0u) { + keys.resize(count); + if (!ReadScalarTable(material.Base48(), material.GetSize(), uint64_t {first} * 4u, + runtime, keys)) return false; + } else { + keys.reserve(count); + for (uint64_t index = first; index < uint64_t {first} + count; ++index) { + uint32_t key = 0; + const auto offset = index * material.Stride() + indirect.selector_offset; + if (!ReadScalarTable(material.Base48(), material.GetSize(), offset, runtime, + {&key, 1})) return false; + keys.push_back(key); + } } - std::ranges::sort(keys); - keys.erase(std::unique(keys.begin(), keys.end()), keys.end()); } else if (!indirect.selector_mask.IsEmpty()) { uint32_t mask = 0; uint32_t count = 0; @@ -330,8 +334,6 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p keys.push_back(key); mask &= mask - 1u; } - std::ranges::sort(keys); - keys.erase(std::unique(keys.begin(), keys.end()), keys.end()); } else { ShaderBufferResource material; if (!DecodeBufferDescriptor(material_value, material) || table_value.dword_count != 4u || @@ -357,6 +359,12 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p } keys.push_back(key); } + } + if (indirect.material_source != UINT32_MAX) { + if (indirect.selector_shift != 0u) { + for (auto& key: keys) key >>= indirect.selector_shift; + keys.push_back(0u); // An out-of-range material load returns zero. + } std::ranges::sort(keys); keys.erase(std::unique(keys.begin(), keys.end()), keys.end()); } @@ -426,10 +434,21 @@ struct SamplerPlan { uint32_t sampler_count = 0; }; +template +void CompactImages(std::vector& images, Keep&& keep) { + size_t count = 0; + for (size_t index = 0; index < images.size(); ++index) { + if (!keep(index)) continue; + if (count != index) images[count] = std::move(images[index]); + ++count; + } + images.resize(count); +} + struct ImageRemap { explicit ImageRemap(const ResourceSpecialization& specialization) - : source_count(static_cast(specialization.images.size())) { - EXIT_IF(specialization.images.size() > indices.size()); + : indices(specialization.images.size()), + source_count(static_cast(specialization.images.size())) { for (uint32_t index = 0; index < source_count; index++) { indices[index] = specialization.images[index].fmask ? UINT32_MAX : count++; } @@ -446,16 +465,11 @@ struct ImageRemap { if (count == source_count) { return; } - for (uint32_t index = 0; index < source_count; index++) { - if (indices[index] != UINT32_MAX && indices[index] != index) { - images[indices[index]] = std::move(images[index]); - } - } - images.resize(count); + CompactImages(images, [&](size_t index) { return indices[index] != UINT32_MAX; }); } private: - std::array indices; + std::vector indices; uint32_t source_count; uint32_t count = 0; }; @@ -602,7 +616,7 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna } } } - ImageRemap(specialization).Apply(snapshot.images); + CompactImages(snapshot.images, [&](size_t index) { return !specialization.images[index].fmask; }); return true; } @@ -1146,7 +1160,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime const auto& indirect = *source->indirect_descriptor; if (!MaterializeIndirectDescriptor( program, indirect, i, 8u, observed, clean, snapshot, snapshot.images, - specialization.images, ShaderInfo::MaxImages, [&](DescriptorValue& value) { + specialization.images, UINT32_MAX, [&](DescriptorValue& value) { if (NullImageDescriptor(value) || !ValidImageDescriptor(value, image.r128)) value.dwords.fill(0u); return true; diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp index cd4210854..6508feb26 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp @@ -826,6 +826,7 @@ private: const auto& b = *descriptor.indirect_descriptor; if (a.material_source != b.material_source || a.table_source != b.table_source || a.selector_stride != b.selector_stride || a.selector_offset != b.selector_offset || + a.selector_shift != b.selector_shift || a.table_offset != b.table_offset || a.sources != b.sources || !EquivalentValue(m_program, a.key_count, b.key_count) || a.selector_first.IsEmpty() != b.selector_first.IsEmpty() || @@ -1549,6 +1550,37 @@ private: return true; } + bool MatchUniformizedBufferKey(Value key, const Inst& image, + DescriptorSource::IndirectDescriptor& indirect, + DescriptorSource& material_source) { + const auto* selected = UniformizedMaterialValue(key, image); + if (selected == nullptr) return false; + const auto* shift = selected->Arg(1).Resolve().TryInstruction(); + uint32_t amount = 0; + if (shift == nullptr || shift->GetOpcode() != ValueOpcode::ShiftRightLogical32 || + !ImmediateU32(shift->Arg(1), amount) || amount == 0u || amount >= 32u) return false; + const auto* loaded = shift->Arg(0).Resolve().TryInstruction(); + if (loaded == nullptr || loaded->GetOpcode() != ValueOpcode::SelectU32 || + !EquivalentValue(m_program, selected->Arg(0), loaded->Arg(0))) return false; + const auto* read = loaded->Arg(1).Resolve().TryInstruction(); + if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32) return false; + const auto& memory = m_program.memory_info[read->Flags().index]; + uint32_t offset = 1u, scalar = 1u; + if (memory.kind != ResourceKind::Buffer || memory.typed || memory.formatted || + !memory.idxen || memory.offen || + !ImmediateU32(read->Arg(2), offset) || offset != 0u || + !ImmediateU32(read->Arg(3), scalar) || scalar != 0u || + !EquivalentValue(m_program, selected->Arg(0), read->Arg(4)) || + !MakeRuntimeTableSource(*read, material_source)) return false; + // GPU index arithmetic may wrap. Every record in the bounded V# is a candidate. + indirect.material_source = InternSource(material_source); + indirect.selector_first = Value(0u); + indirect.key_count = material_source.dwords[2]; + indirect.selector_offset = memory.offset; + indirect.selector_shift = amount; + return true; + } + bool TryMakeIndirectImage(Inst& handle, IndirectDescriptorPlan& plan) { if (handle.GetOpcode() != ValueOpcode::GetImageResource || handle.NumArgs() != 8u) return false; DescriptorSource table_source; @@ -1575,7 +1607,7 @@ private: } if ((table_offset & 3u) != 0u || (bitscan && table_offset > UINT32_MAX - (32u * 32u - 1u))) return false; - } else { + } else if (!MatchUniformizedBufferKey(key, handle, indirect, material_source)) { auto* material_read = key.Resolve().TryInstruction(); uint32_t material_memory_index = 0; const auto* memory = material_read != nullptr diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index f985c3237..fe4e04dcf 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -17035,6 +17035,7 @@ private: features12.samplerMirrorClampToEdge != true || features12.shaderOutputViewportIndex != true || features12.shaderBufferInt64Atomics != true || + features12.shaderSampledImageArrayNonUniformIndexing != true || features12.shaderSharedInt64Atomics != true || image_atomic64.shaderImageInt64Atomics != true || workgroup_layout.workgroupMemoryExplicitLayout != true || @@ -17107,6 +17108,9 @@ private: Require("VulkanHarness", "dispatch", available_features12.bufferDeviceAddress == true, "bufferDeviceAddress is not supported"); + Require("VulkanHarness", "dispatch", + available_features12.shaderSampledImageArrayNonUniformIndexing == true, + "nonuniform sampled image indexing is not supported"); Require("VulkanHarness", "dispatch", available_min_lod.minLod == true, "image view minimum LOD is not supported"); Require("VulkanHarness", "graphics", available_features12.shaderOutputLayer == true, @@ -31364,12 +31368,44 @@ void CheckIndirectImageKeySwitch() { std::string text; Require(name, "SPIR-V disassembly", tools.Disassemble(spirv, &text), "failed to disassemble indirect image shader"); - Require(name, "key switch", - text.find("OpSwitch") != std::string::npos && - text.find("OpPhi") != std::string::npos && - CountText(text, "OpImageSampleExplicitLod") == 2 && - CountText(text, "OpIEqual") == 1, - "dynamic image key did not use a compact two-sample switch"); + Require(name, "homogeneous image array", + text.find("OpSwitch") == std::string::npos && + CountText(text, "OpImageSampleExplicitLod") == 1 && + text.find("SampledImageArrayNonUniformIndexing") != std::string::npos, + "homogeneous image keys did not select one descriptor array sample"); + u32 sampled_image = 0; + std::vector nonuniform; + for (size_t offset = 5; offset < spirv.size();) { + const auto words = std::span(spirv).subspan(offset, spirv[offset] >> 16u); + const auto opcode = static_cast(words[0] & 0xffffu); + if (opcode == spv::OpDecorate && words[2] == spv::DecorationNonUniform) + nonuniform.push_back(words[1]); + if (opcode == spv::OpImageSampleExplicitLod) sampled_image = words[3]; + offset += words.size(); + } + Require(name, "nonuniform sampled image operand", + sampled_image != 0u && std::ranges::find(nonuniform, sampled_image) != nonuniform.end(), + "the sample operand lacks its nonuniform decoration"); + + // Materialized children follow all native roots, including unrelated images. + root.indirect_resources = {0u}; + program.info.images = {root, candidate}; + program.info.images[1].indirect_root = ImageResource::NoIndirectImage; + for (u32 ordinal = 1; ordinal < 189u; ++ordinal) { + root.indirect_resources.push_back(static_cast(program.info.images.size())); + program.info.images.push_back(candidate); + } + program.info.images[0] = root; + program.binding_layout_complete = false; + AllocateBindings(program); + spirv = ShaderRecompiler::Spirv::EmitProgram(program, {.compute = &compute}); + ValidateSpirv(name, spirv); + Require(name, "large image array disassembly", tools.Disassemble(spirv, &text), + "failed to disassemble the materialized image array"); + Require(name, "large homogeneous image array", + text.find("OpSwitch") == std::string::npos && + CountText(text, "OpImageSampleExplicitLod") == 1, + "materialized images expanded into per-candidate samples"); program.memory_info[0].image_dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2DArray; @@ -35553,6 +35589,94 @@ void CheckImageSamplerSpecialization() { "a shared float/unsigned/signed sampler lost its border or filtering " "class in the compiled bindings, instruction or descriptor snapshot"); + { + using namespace ShaderRecompiler::IR; + constexpr const char *name = "GpuMaterialImageDomain"; + constexpr u32 count = 70, table_base = 0x1000, material_base = 0x2000; + std::vector memory(4096); + for (u32 i = 0; i < count; ++i) { + auto descriptor = native_image_descriptor; + descriptor.dwords[0] += i * 0x100u; + std::copy(descriptor.dwords.begin(), descriptor.dwords.end(), + memory.begin() + table_base / 4u + i * 8u); + memory[material_base / 4u + i] = (i << 4u) | 3u; + } + memory[material_base / 4u + count] = (7u << 4u) | 15u; // Duplicate after shift. + memory[material_base / 4u + count + 1u] = 0x80000010u; // key<<5 wraps to entry1. + memory[material_base / 4u + count + 2u] = count << 4u; // Scalar table OOB is null. + std::array user_data{}; + const std::array buffers{table_base, 32u << 16u, count, 0x5204u, + material_base, 4u << 16u, count + 3u, 0x5204u}; + std::copy(buffers.begin(), buffers.end(), user_data.begin()); + const auto output = MakeStructuredStorageBufferData( + 0u, 64u, false, static_cast(Prospero::BufferFormat::k32UInt)); + std::copy_n(output.begin(), 4, user_data.begin() + 48); + user_data[48] = 0x3000u; + std::vector code{EncodeVopc(0xc5, InlineU32(0), 0), + EncodeSop1(0x24, 24, 106), EncodeSopp(0x08, 0), + EncodeMubuf0(0x0c, 0, true, false), EncodeMubuf1(15, 1, 0), + EncodeVop2(0x16, 15, InlineU32(4), 15), + EncodeVop1(0x02, 20, Vgpr(15)), EncodeVopc(0xd2, 20, 15), + EncodeSop2(0x1e, 20, 20, InlineU32(5)), + EncodeSmem0(0x0b, 12), EncodeSmem1(0, 20), + EncodeMimg0(0x24, 1), EncodeMimg1(4, 0, 3, 2)}; + AppendBufferStoreDword(&code, 4, 0); + code[2] = EncodeSopp(0x08, static_cast(code.size() - 3u)); + AppendEnd(&code); + ShaderComputeInputInfo compute{}; + compute.wave_size = 32; + compute.threads_num[0] = 32; + compute.thread_ids_num = 1; + ShaderRecompiler::CompileOptions options; + options.stage = ShaderType::Compute; + options.wave_size = 32; + options.user_data = user_data; + options.input_info.compute = &compute; + auto translated = ShaderRecompiler::TranslateProgram(code, options); + auto plan = ExtractResourcePlan(translated.program); + ResourceSnapshot snapshot; + ResourceSpecialization specialization; + const SrtRuntime runtime{.user_data = user_data, .read_memory = ReadTestMemory, + .userdata = &memory, .read_specialization_memory = ReadTestMemory}; + Require(name, "bounded native material plan", + plan.capture_specialization_reads && !translated.program.has_address_writes && + MaterializeResources(plan, runtime, snapshot, specialization) && + snapshot.images.size() == count + 1u, + "GPU-selected material descriptors did not expand beyond native root capacity"); + const auto offset = specialization.images[0].indirect_mapping_offset; + const auto ordinal = [&](u32 key) { + for (u32 i = 0; i < snapshot.flattened_srt[offset]; ++i) + if (snapshot.flattened_srt[offset + 1u + 2u * i] == key) + return snapshot.flattened_srt[offset + 2u + 2u * i]; + return UINT32_MAX; + }; + Require(name, "shift, wrap and whole-record read", + snapshot.flattened_srt[offset] == count + 2u && ordinal(1u) != UINT32_MAX && + ordinal(0x08000001u) == ordinal(1u) && ordinal(count) < snapshot.images.size() && + snapshot.images[ordinal(count)].dwords[0] == 0u && + std::ranges::count(snapshot.specialization_reads, + std::pair{material_base, (count + 3u) * 4u}) == 1, + "material keys lost shift/dedup, scalar wrap/OOB, or bulk strict-read semantics"); + const auto previous = specialization; + memory[table_base / 4u + 8u] += 0x10000u; + Require(name, "runtime descriptor refresh", + MaterializeResources(plan, runtime, snapshot, specialization) && + specialization == previous && snapshot.images[1].dwords[0] == 0x11100u, + "descriptor addresses were frozen or entered the shader specialization"); + auto compiled = ShaderRecompiler::CompileProgram(std::move(translated), options, specialization); + ValidateSpirv(name, compiled.spirv); + user_data[4] += 1u; + Require(name, "misaligned material rejection", + !MaterializeResources(plan, runtime, snapshot, specialization), + "unaligned raw buffer base was treated as separately aligned scalar memory"); + user_data[4] -= 1u; + auto dirty = runtime; + dirty.read_specialization_memory = +[](void*, uint64_t, std::span) { return false; }; + Require(name, "dirty material rejection", + !MaterializeResources(plan, dirty, snapshot, specialization), + "GPU-selected texture materialization bypassed strict read provenance"); + } + std::printf("[host] %-32s ok\n", "ImageSpecializationPipelineId"); }