#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h" #include "common/assert.h" #include "graphics/guest_gpu/gpu_format.h" #include "graphics/shader/recompiler/BufferFormat.h" #include "graphics/shader/recompiler/ir/ShaderIR.h" #include "graphics/shader/shaderBindings.h" #include #include #include #include #include #include #include #include #include namespace Libs::Graphics::ShaderRecompiler::IR { namespace { constexpr uint64_t AddressMask = 0x0000ffffffffffffull; constexpr uint64_t MaxIndirectDescriptorProbes = 65536u; bool SpecializationFail(std::string_view message) { std::fprintf(stderr, "shader resource specialization failed: %.*s\n", static_cast(message.size()), message.data()); return false; } Decoder::ImageDimension DescriptorDimension(const DescriptorValue& descriptor, Decoder::ImageDimension requested) { const bool is_array = requested == Decoder::ImageDimension::Dim1DArray || requested == Decoder::ImageDimension::Dim2DArray || requested == Decoder::ImageDimension::Dim2DMsaaArray; switch (static_cast((descriptor.dwords[3] >> 28u) & 0xfu)) { case Prospero::ImageType::kColor1D: return Decoder::ImageDimension::Dim1D; case Prospero::ImageType::kColor1DArray: if (is_array) { return Decoder::ImageDimension::Dim1DArray; } return Decoder::ImageDimension::Dim1D; case Prospero::ImageType::kColor3D: return Decoder::ImageDimension::Dim3D; case Prospero::ImageType::kCube: return Decoder::ImageDimension::Dim2DArray; case Prospero::ImageType::kColor2DArray: if (is_array) { return Decoder::ImageDimension::Dim2DArray; } return Decoder::ImageDimension::Dim2D; case Prospero::ImageType::kColor2DMsaaArray: if (is_array) { return Decoder::ImageDimension::Dim2DMsaaArray; } return Decoder::ImageDimension::Dim2DMsaa; case Prospero::ImageType::kColor2D: return Decoder::ImageDimension::Dim2D; case Prospero::ImageType::kColor2DMsaa: return Decoder::ImageDimension::Dim2DMsaa; default: return Decoder::ImageDimension::Unknown; } } bool NullImageDescriptor(const DescriptorValue& descriptor) { return descriptor.dwords[0] == 0 && (descriptor.dwords[1] & 0xffu) == 0; } bool ValidImageDescriptor(const DescriptorValue& descriptor, bool r128 = false) { const auto& words = descriptor.dwords; // Reject texture descriptors with nonzero reserved bits. if ((words[1] & 0x20000000u) != 0u || (words[2] & 0x70003000u) != 0u || (!r128 && ((words[4] & 0xe000e000u) != 0u || (words[5] & 0xf9000000u) != 0u || (words[6] & 0x00007b00u) != 0u))) { return false; } const auto type = static_cast((descriptor.dwords[3] >> 28u) & 0xfu); const auto format = static_cast((descriptor.dwords[1] >> 20u) & 0x1ffu); if (type < Prospero::ImageType::kColor1D || format == Prospero::BufferFormat::kInvalid || format > Prospero::BufferFormat::kBc7Srgb) { return false; } if (r128 && type != Prospero::ImageType::kColor1D && type != Prospero::ImageType::kColor2D && type != Prospero::ImageType::kColor2DMsaa) { return false; } const bool array = type == Prospero::ImageType::kColor1DArray || type == Prospero::ImageType::kColor2DArray || type == Prospero::ImageType::kColor2DMsaaArray || type == Prospero::ImageType::kCube; if (array && ((words[4] >> 16u) & 0x1fffu) > (words[4] & 0x1fffu)) { return false; } if (type == Prospero::ImageType::kColor2DMsaa || type == Prospero::ImageType::kColor2DMsaaArray) { const auto base_level = (descriptor.dwords[3] >> 12u) & 0xfu; const auto fragments = (descriptor.dwords[3] >> 16u) & 0xfu; const auto max_mip = (descriptor.dwords[5] >> 4u) & 0xfu; return base_level == 0 && fragments >= 1 && fragments <= 3 && (r128 || max_mip == fragments); } return true; } uint32_t DescriptorImageSwizzle(const DescriptorValue& descriptor) { return descriptor.dwords[3] & 0xfffu; } Prospero::BufferFormat ImageConversionFormat(Prospero::BufferFormat format) { return Prospero::RemapTextureFormat(format) != format ? format : Prospero::BufferFormat::kInvalid; } enum class SamplerClass : uint8_t { Float, Integer, PointInteger }; SamplerClass ClassifySampler(const ImageResource& image) { if (image.numeric_class == Prospero::TextureNumericClass::Sint || (image.numeric_class == Prospero::TextureNumericClass::Uint && image.conversion_format != Prospero::BufferFormat::kInvalid)) { return SamplerClass::PointInteger; } return image.numeric_class == Prospero::TextureNumericClass::Uint ? SamplerClass::Integer : SamplerClass::Float; } bool DescriptorIsCube(const DescriptorValue& descriptor) { return static_cast((descriptor.dwords[3] >> 28u) & 0xfu) == Prospero::ImageType::kCube; } uint32_t ImageMipCount(const ImageResource& image, const DescriptorValue& descriptor) { if (image.mip_mode != ImageMipMode::Dynamic || NullImageDescriptor(descriptor)) { return 1; } const auto base = (descriptor.dwords[3] >> 12u) & 0xfu; const auto last = (descriptor.dwords[3] >> 16u) & 0xfu; return base <= last ? last - base + 1u : 0u; } bool DecodeBufferDescriptor(const DescriptorValue& descriptor, ShaderBufferResource& result) { if (descriptor.dword_count != std::size(result.fields)) { return false; } std::copy_n(descriptor.dwords.begin(), std::size(result.fields), result.fields); return true; } struct ReadCapture { SrtRuntime source; std::vector>& ranges; }; bool CaptureStrictRead(void* userdata, uint64_t address, std::span values) { auto& capture = *static_cast(userdata); if (!capture.source.read_specialization_memory(capture.source.userdata, address, values)) { return false; } capture.ranges.emplace_back(address, values.size_bytes()); return true; } bool CaptureOrdinaryRead(void* userdata, uint64_t address, std::span values) { auto& capture = *static_cast(userdata); if (capture.source.read_memory != nullptr) { if (!capture.source.read_memory(capture.source.userdata, address, values)) return false; } else { std::memcpy(values.data(), reinterpret_cast(address), values.size_bytes()); } capture.ranges.emplace_back(address, values.size_bytes()); return true; } const DescriptorSource* Source(const ResourcePlan& program, uint32_t source) { if (source >= program.descriptor_sources.size()) { return nullptr; } return &program.descriptor_sources[source]; } void MarkCleanFlatSlots(const ResourcePlan& program, const DescriptorSource* source, std::span slots, Value extra = {}) { if (source == nullptr && extra.IsEmpty()) { return; } std::vector pending; if (source != nullptr) { pending.assign(source->dwords.begin(), source->dwords.begin() + source->dword_count); } if (!extra.IsEmpty()) pending.push_back(extra); std::vector visited; while (!pending.empty()) { auto value = pending.back().Resolve(); pending.pop_back(); const auto* inst = value.TryInstruction(); if (inst == nullptr || std::ranges::find(visited, inst) != visited.end()) { continue; } visited.push_back(inst); if (inst->GetOpcode() == ValueOpcode::ReadConst) { const auto slot = inst->Arg(1).Resolve(); if (slot.IsImmediate() && slot.GetType() == Type::U32 && slot.U32() < slots.size()) { slots[slot.U32()] = 1u; pending.push_back(program.srt_reads[slot.U32()].value); } continue; } for (size_t arg = 0; arg < inst->NumArgs(); arg++) { pending.push_back(inst->Arg(arg)); } } } bool ReadScalarTable(uint64_t base, uint64_t size, uint64_t dynamic_offset, const SrtRuntime& runtime, std::span words) { const auto offset = dynamic_offset & ~uint64_t {3}; const auto count = std::min(words.size(), offset < size ? (size - offset) / 4u : 0u); std::ranges::fill(words.subspan(count), 0u); if (count == 0u) { return true; } base &= AddressMask & ~uint64_t {3}; if (offset > AddressMask - base) { return false; } const auto address = base + offset; const auto prefix = words.first(count); return prefix.size_bytes() - 1u <= AddressMask - address && runtime.read_specialization_memory != nullptr && runtime.read_specialization_memory(runtime.userdata, address, prefix); } bool NormalizeIndirectStoreBuffer(DescriptorValue& value) { ShaderBufferResource descriptor; if (!DecodeBufferDescriptor(value, descriptor)) return false; if (descriptor.Type() != 0u || descriptor.Format() == Prospero::BufferFormat::kInvalid || descriptor.Base48() == 0u || descriptor.GetSize() == 0u) { value.dwords.fill(0u); } else if (descriptor.Stride() != 1u || descriptor.SwizzleEnabled() || descriptor.AddTid() || descriptor.OutOfBounds() != 3u) { return SpecializationFail("indirect buffer stores require linear byte bounds"); } return true; } template bool MaterializeIndirectDescriptor(const ResourcePlan& program, const DescriptorSource::IndirectDescriptor& indirect, uint32_t resource_index, uint32_t dword_count, 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& sources = indirect.sources; auto& keys = program.material_keys; const auto read_keys = [&](const ShaderBufferResource& material, uint64_t first, uint64_t step, uint64_t count) { keys.resize(count); if (step == 4u) { return ReadScalarTable(material.Base48(), material.GetSize(), first, runtime, keys); } for (auto& key: keys) { if (!ReadScalarTable(material.Base48(), material.GetSize(), first, runtime, {&key, 1})) return false; first += step; } return true; }; uint64_t table_base = 0; uint64_t table_size = UINT64_MAX; // Scalar addresses have no buffer descriptor bounds. if (sources.empty()) { keys.clear(); DescriptorValue material_value; DescriptorValue table_value; if ((indirect.material_source != UINT32_MAX && !clean.EvaluateDescriptor(indirect.material_source, material_value)) || !clean.EvaluateDescriptor(indirect.table_source, table_value)) { return false; } 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; } if (indirect.material_source == UINT32_MAX) { uint32_t key_count = 0; if (indirect.workgroup_axis != UINT32_MAX) { if (indirect.workgroup_axis >= runtime.workgroup_counts.size()) return false; key_count = runtime.workgroup_counts[indirect.workgroup_axis]; if (key_count == 0u) return false; } else { if (table_value.dword_count != 2u || !clean.Evaluate(indirect.key_count, key_count)) return false; if (std::bit_cast(key_count) <= 0) key_count = 0; } if (indirect.table_stride == 0u || key_count > MaxIndirectDescriptorProbes || (key_count != 0u && uint64_t {indirect.table_offset} + uint64_t {key_count - 1u} * indirect.table_stride + descriptor_bytes > UINT32_MAX + 1ull)) { return false; } keys.resize(key_count); std::iota(keys.begin(), keys.end(), 0u); } else if (!indirect.selector_first.IsEmpty()) { 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() || !clean.Evaluate(indirect.selector_first, first) || !clean.Evaluate(indirect.key_count, count) || count > MaxIndirectDescriptorProbes || uint64_t {first} + count > material.NumRecords()) return false; if (count != 0u && (uint64_t {first} + count - 1u) * material.Stride() + indirect.selector_offset + 4u > uint64_t {UINT32_MAX} + 1u) return false; if (!read_keys(material, uint64_t {first} * material.Stride() + indirect.selector_offset, material.Stride(), count)) return false; } else if (!indirect.selector_mask.IsEmpty()) { uint32_t mask = 0; uint32_t count = 0; if (material_value.dword_count != 2u || table_value.dword_count != 2u || !clean.Evaluate(indirect.selector_mask, mask) || !clean.Evaluate(indirect.key_count, count) || count == 0u || count > 32u) { return false; } if (count < 32u) mask &= (1u << count) - 1u; const auto material_base = (static_cast(material_value.dwords[1]) << 32u) | material_value.dwords[0]; keys.reserve(std::popcount(mask)); while (mask != 0u) { const auto index = std::countr_zero(mask); const auto offset = static_cast(indirect.selector_offset) + static_cast(index) * indirect.selector_stride; if (offset > UINT32_MAX) return false; uint32_t key = 0; if (!ReadScalarTable(material_base, UINT64_MAX, static_cast(offset), runtime, {&key, 1})) return false; keys.push_back(key); mask &= mask - 1u; } } else { ShaderBufferResource material; if (!DecodeBufferDescriptor(material_value, material) || table_value.dword_count != 4u) { return false; } // The first aligned offset includes the immediate added after shader U32 arithmetic. const auto step = std::max(4u, std::gcd(indirect.selector_stride, uint64_t {1} << 32u)); const uint64_t first = indirect.selector_offset; const auto size = material.GetSize(); const auto limit = std::min(first + (uint64_t {1} << 32u) - step, size >= 4u ? size - 4u : 0u); const auto probe_count = size >= 4u && first <= limit ? (limit - first) / step + 1u : 0u; if (probe_count > MaxIndirectDescriptorProbes) { return false; } if (!read_keys(material, first, step, probe_count)) return false; if (indirect.selector_shift == 0u) keys.push_back(0u); } 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()); } } const auto children_begin = descriptors.size(); const auto mapping_offset = snapshot.flattened_srt.size(); const auto root_resource = specializations[resource_index]; const auto key_count = sources.empty() ? keys.size() : sources.size(); snapshot.flattened_srt.resize(mapping_offset + 1u + key_count * 2u); snapshot.flattened_srt[mapping_offset] = static_cast(key_count); for (uint32_t entry = 0; entry < key_count; ++entry) { const auto key = sources.empty() ? keys[entry] : entry; DescriptorValue candidate; candidate.dword_count = dword_count; if (sources.empty()) { const auto table_offset = static_cast(key * indirect.table_stride) + indirect.table_offset; if (!ReadScalarTable(table_base, table_size, table_offset, runtime, std::span(candidate.dwords).first(dword_count))) { return false; } } else if (!clean.EvaluateDescriptor(sources[entry], candidate)) { return false; } if (!normalize(candidate)) return false; uint32_t ordinal = 0; if (entry == 0) { descriptors[resource_index] = candidate; } else if (descriptors[resource_index] != candidate) { const auto found = std::find(descriptors.begin() + children_begin, descriptors.end(), candidate); ordinal = static_cast(found - descriptors.begin() - children_begin + 1u); if (found == descriptors.end()) { if (descriptors.size() >= maximum_resources) { return false; } descriptors.push_back(candidate); auto child = root_resource; child.indirect_root = resource_index; specializations.push_back(child); } } snapshot.flattened_srt[mapping_offset + 1u + entry * 2u] = key; snapshot.flattened_srt[mapping_offset + 2u + entry * 2u] = ordinal; } if (descriptors.size() == children_begin) { snapshot.flattened_srt.resize(mapping_offset); } else { auto& root = specializations[resource_index]; root.indirect_root = resource_index; root.indirect_mapping_offset = static_cast(mapping_offset); root.indirect_search_iterations = std::bit_width(key_count); } return true; } } // namespace struct SamplerPlan { struct Binding { uint32_t source; SamplerClass type; }; std::array, ShaderInfo::MaxSamplers> mapping; std::array bindings; 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) : 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++; } } uint32_t operator[](uint32_t index) const { EXIT_IF(index >= source_count); return indices[index]; } template void Apply(std::vector& images) const { EXIT_IF(images.size() != source_count); if (count == source_count) { return; } CompactImages(images, [&](size_t index) { return indices[index] != UINT32_MAX; }); } private: std::vector indices; uint32_t source_count; uint32_t count = 0; }; static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSnapshot& snapshot, ResourceSpecialization& specialization) { for (uint32_t i = 0; i < specialization.images.size(); i++) { const auto& descriptor = snapshot.images[i]; auto& image = specialization.images[i]; const auto base_index = i < program.info.images.size() ? i : image.indirect_root; if (base_index >= program.info.images.size()) { return SpecializationFail(fmt::format("image resource {} has an invalid root", i)); } const auto& base = program.info.images[base_index]; if (base.resource_class == ImageResourceClass::None || (base.atomic && base.resource_class != ImageResourceClass::Storage)) { return SpecializationFail(fmt::format("image resource {} has an invalid class", i)); } image.mip_count = ImageMipCount(base, descriptor); if (image.mip_count == 0u) { return SpecializationFail( fmt::format("image descriptor {} has an invalid mip range", i)); } if (NullImageDescriptor(descriptor)) { image.numeric_class = base.atomic ? Prospero::TextureNumericClass::Uint : Prospero::TextureNumericClass::Float; image.dimension = Decoder::ImageDimension::Dim2D; image.cube = false; continue; } const auto descriptor_dimension = DescriptorDimension(descriptor, base.dimension); if (descriptor_dimension == Decoder::ImageDimension::Unknown) { return SpecializationFail(fmt::format( "image descriptor {} has unsupported type {}: {:08x},{:08x},{:08x},{:08x}," "{:08x},{:08x},{:08x},{:08x}", i, (descriptor.dwords[3] >> 28u) & 0xfu, descriptor.dwords[0], descriptor.dwords[1], descriptor.dwords[2], descriptor.dwords[3], descriptor.dwords[4], descriptor.dwords[5], descriptor.dwords[6], descriptor.dwords[7])); } image.dimension = descriptor_dimension; image.cube = DescriptorIsCube(descriptor); const auto format = static_cast((descriptor.dwords[1] >> 20u) & 0x1ffu); if (base.atomic && (base.atomic64 ? format != Prospero::BufferFormat::k32_32UInt : format != Prospero::BufferFormat::k32UInt && format != Prospero::BufferFormat::k32Float)) { return SpecializationFail( fmt::format("atomic image descriptor {} uses unsupported format {}", i, static_cast(format))); } const bool storage = base.resource_class == ImageResourceClass::Storage; image.fmask = Prospero::IsFmaskTextureFormat(format); if (image.fmask) { if (storage || base.depth_compare || image.indirect_root != ImageResource::NoIndirectImage || std::ranges::any_of(program.info.sampled_pairs, [&](const auto& pair) { return pair.image == i; })) { return SpecializationFail("FMASK requires a direct image load"); } } image.conversion_format = ImageConversionFormat(format); if (storage || image.conversion_format != Prospero::BufferFormat::kInvalid) { image.shader_swizzle = DescriptorImageSwizzle(descriptor); } const bool raw_sint_storage = storage && format == Prospero::BufferFormat::k32SInt && base.written && !base.read && !base.atomic; image.numeric_class = Prospero::SampledTextureNumericClass(format); if (storage) { if ((!raw_sint_storage && image.numeric_class == Prospero::TextureNumericClass::Sint) || image.numeric_class == Prospero::TextureNumericClass::Unsupported) { return SpecializationFail( fmt::format("storage image descriptor {} uses unsupported format {}", i, static_cast(format))); } if (raw_sint_storage || base.atomic) { image.numeric_class = Prospero::TextureNumericClass::Uint; } } else if (image.numeric_class == Prospero::TextureNumericClass::Unsupported || (base.depth_compare && image.numeric_class != Prospero::TextureNumericClass::Float)) { return SpecializationFail( fmt::format("sampled image descriptor {} uses unsupported format {}", i, static_cast(format))); } } for (uint32_t root_index = 0; root_index < specialization.images.size(); root_index++) { auto& root = specialization.images[root_index]; if (root.indirect_root != root_index) { continue; } const auto key_count = root.indirect_mapping_offset < snapshot.flattened_srt.size() ? snapshot.flattened_srt[root.indirect_mapping_offset] : 0u; if (root.indirect_search_iterations == 0u || key_count < 2u || static_cast(root.indirect_mapping_offset) + 1u + static_cast(key_count) * 2u > snapshot.flattened_srt.size()) { return SpecializationFail("indirect image specialization has an invalid key mapping"); } uint32_t exemplar = ImageResource::NoIndirectImage; uint32_t resource_count = 0; for (uint32_t resource = 0; resource < specialization.images.size(); resource++) { if (specialization.images[resource].indirect_root != root_index) { continue; } resource_count++; if (exemplar == ImageResource::NoIndirectImage && !NullImageDescriptor(snapshot.images[resource])) { exemplar = resource; } } if (resource_count < 2u || exemplar == ImageResource::NoIndirectImage) { return SpecializationFail("indirect image specialization has no typed candidate"); } const auto& image_class = specialization.images[exemplar]; const auto is_2d = [](Decoder::ImageDimension dimension) { return dimension == Decoder::ImageDimension::Dim2D || dimension == Decoder::ImageDimension::Dim2DArray; }; for (uint32_t candidate = 0; candidate < specialization.images.size(); candidate++) { auto& image = specialization.images[candidate]; if (image.indirect_root != root_index) { continue; } if (NullImageDescriptor(snapshot.images[candidate])) { image.numeric_class = image_class.numeric_class; image.dimension = image_class.dimension; image.mip_count = image_class.mip_count; image.conversion_format = image_class.conversion_format; image.shader_swizzle = image_class.shader_swizzle; image.cube = image_class.cube; } const bool same_coordinates = image.dimension == image_class.dimension && image.cube == image_class.cube; if (image.numeric_class != image_class.numeric_class || (!same_coordinates && !(is_2d(image.dimension) && is_2d(image_class.dimension))) || image.mip_count != image_class.mip_count || image.conversion_format != image_class.conversion_format || image.shader_swizzle != image_class.shader_swizzle) { return SpecializationFail( fmt::format("indirect image table at pc 0x{:08x} has incompatible candidates", program.info.images[root_index].first_use_pc)); } } } CompactImages(snapshot.images, [&](size_t index) { return !specialization.images[index].fmask; }); return true; } bool BuildSamplerPlan(const ShaderInfo& base, SamplerPlan& plan) { if (base.samplers.size() > plan.mapping.size()) { return false; } std::array usage {}; plan.sampler_count = static_cast(base.samplers.size()); for (const auto& pair: base.sampled_pairs) { if (pair.image >= base.images.size() || pair.sampler >= base.samplers.size()) { return false; } usage[pair.sampler] |= 1u << static_cast(ClassifySampler(base.images[pair.image])); } for (uint32_t index = 0; index < base.samplers.size(); index++) { auto& mapping = plan.mapping[index]; mapping.fill(UINT32_MAX); const auto classes = usage[index] == 0u ? 1u : usage[index]; bool first = true; for (uint32_t type = 0; type < mapping.size(); type++) { if ((classes & (1u << type)) == 0u) continue; const auto target = first ? index : plan.sampler_count++; if (target >= ShaderInfo::MaxSamplers) return false; mapping[type] = target; plan.bindings[target] = {index, static_cast(type)}; first = false; } } return true; } template static std::vector ResourceControlFlow(const Program& program, const Predicate& predicate) { // The renderer checks captured scalar reads against final buffer and image write ranges. if (program.blocks.size() != program.block_info.size() || program.has_address_writes) { return {}; } std::unordered_map indices; for (uint32_t i = 0; i < program.block_info.size(); i++) { if (!indices.emplace(program.block_info[i].id, i).second) { return {}; } } std::vector blocks(program.blocks.size()); std::vector> predecessors(blocks.size()); for (uint32_t i = 0; i < blocks.size(); i++) { auto& block = blocks[i]; const auto& info = program.block_info[i]; const auto& terminator = info.terminator; std::vector successors; switch (terminator.kind) { case CFG::TerminatorKind::Branch: successors.push_back(terminator.true_block); break; case CFG::TerminatorKind::ConditionalBranch: successors = {terminator.true_block, terminator.false_block}; block.condition = info.condition; break; case CFG::TerminatorKind::IndirectBranch: successors = terminator.indirect_targets; break; case CFG::TerminatorKind::Return: break; default: return {}; } for (const auto successor: successors) { const auto found = indices.find(successor); if (found == indices.end()) { return {}; } block.successors.push_back(found->second); predecessors[found->second].push_back(i); } for (const auto& inst: *program.blocks[i]) { const auto op = inst.GetOpcode(); if (op == ValueOpcode::ReadConst) { block.srt_reads.push_back(inst.Arg(1).Resolve().U32()); continue; } const auto buffer = BufferAccessOf(op); const auto image = ImageOpcodeInfoOf(op); if (buffer == BufferAccess::None && image.access == ImageAccess::None) { continue; } const auto& memory = program.memory_info.at(inst.Flags().index); if (memory.planning_only || memory.kind == ResourceKind::IndirectBuffer) { continue; } if (buffer != BufferAccess::None) { block.sources.push_back(program.info.buffers.at(memory.resource).source); } else { block.sources.push_back(program.info.images.at(memory.resource).source); if (image.needs_sampler) { block.sources.push_back(program.info.samplers.at(memory.sampler).source); } } } std::ranges::sort(block.sources); block.sources.erase(std::unique(block.sources.begin(), block.sources.end()), block.sources.end()); } // Host branches only matter when they guard a later resource access. Leave // data-only branches (for example a mutable flag checked before a trap) on the GPU. std::vector resource_reachable(blocks.size()); std::vector pending; for (uint32_t i = 0; i < blocks.size(); ++i) { if (!blocks[i].sources.empty() || !blocks[i].srt_reads.empty()) { resource_reachable[i] = 1; pending.push_back(i); } } while (!pending.empty()) { const auto index = pending.back(); pending.pop_back(); for (const auto predecessor: predecessors[index]) { if (resource_reachable[predecessor]) continue; resource_reachable[predecessor] = 1; pending.push_back(predecessor); } } for (auto& block: blocks) { block.condition = std::ranges::any_of(block.successors, [&](uint32_t successor) { return resource_reachable[successor] != 0; }) ? predicate(block.condition) : Value {}; } if (std::ranges::none_of( blocks, [](const ResourceBlock& block) { return !block.condition.IsEmpty(); })) { return {}; } return blocks; } // ResolveComputeBufferFill proves complete workgroups and matching requested extents, so every // dispatch-bound predicate accepted here is true for every invocation covered by the fill. static bool IsFullDispatchPredicate(Value value, uint32_t depth = 0) { value = value.Resolve(); if (value == Value(true)) return true; const auto* inst = value.TryInstruction(); if (inst == nullptr || depth > 32) return false; if (inst->GetOpcode() == ValueOpcode::LogicalAnd) { return IsFullDispatchPredicate(inst->Arg(0), depth + 1) && IsFullDispatchPredicate(inst->Arg(1), depth + 1); } if (inst->GetOpcode() != ValueOpcode::ULessThan32) return false; const auto* global = inst->Arg(0).Resolve().TryInstruction(); const auto* extent = inst->Arg(1).Resolve().TryInstruction(); if (global == nullptr || global->GetOpcode() != ValueOpcode::GetBuiltin || global->Arg(0).Resolve() != Value(static_cast(StageInputKind::GlobalInvocationId)) || extent == nullptr || extent->GetOpcode() != ValueOpcode::GetDispatchThreadExtent) return false; const auto axis = global->Arg(1).Resolve(); return axis.IsImmediate() && axis.U32() < 3 && extent->Arg(0).Resolve() == axis; } // Nonnegative affine coefficients for constant, local and workgroup coordinates. Reject modular // arithmetic that could wrap; runtime coverage also bounds the largest invocation index. static std::optional> FillIndex(Value value, uint32_t axis, Value guard, uint32_t depth = 0) { value = ResolveActiveU32(value, guard); if (depth > 32 || value.GetType() != Type::U32) { return {}; } if (value.IsImmediate()) { return std::array {value.U32(), 0, 0}; } const auto* inst = value.TryInstruction(); if (inst == nullptr) { return {}; } const auto op = inst->GetOpcode(); if (op == ValueOpcode::GetBuiltin && inst->Arg(1) == Value(axis)) { if (inst->Arg(0) == Value(static_cast(StageInputKind::LocalInvocationId))) { return std::array {0, 1, 0}; } if (inst->Arg(0) == Value(static_cast(StageInputKind::WorkgroupId))) { return std::array {0, 0, 1}; } } if (op != ValueOpcode::IAdd32 && op != ValueOpcode::IMul32 && op != ValueOpcode::ShiftLeftLogical32) { return {}; } auto left = FillIndex(inst->Arg(0), axis, guard, depth + 1); auto right = FillIndex(inst->Arg(1), axis, guard, depth + 1); if (!left || !right) { return {}; } if (op == ValueOpcode::IMul32 && ((*right)[1] != 0 || (*right)[2] != 0)) { std::swap(left, right); } if (op != ValueOpcode::IAdd32 && ((*right)[1] != 0 || (*right)[2] != 0)) { return {}; } if (op == ValueOpcode::ShiftLeftLogical32) { if ((*right)[0] >= 32) return {}; (*right)[0] = uint64_t {1} << (*right)[0]; } for (uint32_t i = 0; i < left->size(); ++i) { (*left)[i] = op == ValueOpcode::IAdd32 ? (*left)[i] + (*right)[i] : (*left)[i] * (*right)[0]; if ((*left)[i] > UINT32_MAX) return {}; } return left; } static UniformFillPlan AnalyzeUniformFill(const Program& program) { if (program.stage != ShaderType::Compute || program.blocks.empty() || program.blocks.size() != program.block_info.size() || program.info.uses_dma || !program.info.samplers.empty()) { return {}; } std::unordered_set visited; uint32_t index = 0; const Inst* store = nullptr; for (;;) { if (!visited.insert(index).second) return {}; for (const auto& inst: *program.blocks[index]) { if (AddressOpcodeInfoOf(inst.GetOpcode()).access != AddressAccess::None) return {}; if (!inst.MayHaveSideEffects()) continue; if (store != nullptr || (BufferAccessOf(inst.GetOpcode()) != BufferAccess::Write && inst.GetOpcode() != ValueOpcode::ImageWrite)) return {}; store = &inst; } const auto& term = program.block_info[index].terminator; if (term.kind == CFG::TerminatorKind::Return) break; if (term.kind != CFG::TerminatorKind::Branch) return {}; const auto next = std::ranges::find(program.block_info, term.true_block, &BlockInfo::id); if (next == program.block_info.end()) return {}; index = static_cast(next - program.block_info.begin()); } if (store == nullptr || visited.size() != program.blocks.size()) return {}; for (const auto& buffer: program.info.buffers) { if (buffer.read && (!buffer.scalar || buffer.written)) return {}; } const auto& memory = program.memory_info.at(store->Flags().index); UniformFillPlan result; result.fill.resource = memory.resource; Value data; Value guard(true); if (store->GetOpcode() == ValueOpcode::ImageWrite) { if (program.info.images.size() != 1 || memory.dmask != 1 || memory.data_bits != 32 || memory.image_has_mip || memory.image_sample_flags != 0 || memory.image_r128 || memory.image_dimension != Decoder::ImageDimension::Dim2DArray || store->Arg(3).Resolve() != Value(true)) return {}; const auto& image = program.info.images[memory.resource]; if (image.read || image.atomic || image.mip_mode != ImageMipMode::None) return {}; const auto* address = store->Arg(1).ResolveInstruction(); if (address == nullptr || address->GetOpcode() != ValueOpcode::MakeImageAddress) return {}; for (uint32_t axis = 0; axis < 3; ++axis) { const auto index = FillIndex(address->Arg(axis), axis, guard); if (!index || (*index)[0] != 0) return {}; if (axis < 2) { if ((*index)[1] != 1 || (*index)[2] == 0) return {}; } else if ((*index)[1] != 0 || (*index)[2] != 1) { return {}; } result.fill.group_stride[axis] = static_cast((*index)[2]); } const auto* values = store->Arg(2).ResolveInstruction(); if (values == nullptr || values->GetOpcode() != ValueOpcode::CompositeConstructU32x4) return {}; result.fill.kind = UniformFillKind::Image; result.fill.words = 1; data = values->Arg(0); } else { if (!program.info.images.empty()) return {}; const auto op = store->GetOpcode(); constexpr std::array stores {ValueOpcode::StoreBufferU32, ValueOpcode::StoreBufferU32x2, ValueOpcode::StoreBufferU32x3, ValueOpcode::StoreBufferU32x4}; const auto store_op = std::ranges::find(stores, op); if (store_op == stores.end() || store->Arg(2).Resolve() != Value(0u) || store->Arg(3).Resolve() != Value(0u)) return {}; guard = store->Arg(5).Resolve(); if (!IsFullDispatchPredicate(guard)) return {}; if (!memory.formatted || memory.typed || !memory.idxen || memory.offen || memory.offset != 0 || memory.data_bits != 32 || memory.data_dwords != static_cast(store_op - stores.begin() + 1)) return {}; const auto address = FillIndex(store->Arg(1), 0, guard); if (!address || (*address)[0] != 0 || (*address)[1] != 1 || (*address)[2] == 0) return {}; result.fill.kind = UniformFillKind::Buffer; result.fill.group_stride[0] = static_cast((*address)[2]); result.fill.words = memory.data_dwords; data = store->Arg(4); } data = ResolveActiveU32(data, guard); const auto* vector = data.TryInstruction(); constexpr std::array composites {ValueOpcode::CompositeConstructU32x2, ValueOpcode::CompositeConstructU32x3, ValueOpcode::CompositeConstructU32x4}; if (result.fill.words > 1 && (vector == nullptr || vector->GetOpcode() != composites[result.fill.words - 2])) return {}; for (uint32_t i = 0; i < result.fill.words; ++i) { const auto word = ResolveActiveU32(result.fill.words == 1 ? data : vector->Arg(i), guard); if (word.GetType() != Type::U32 || !ValidateRuntimeValue(program, word, RuntimeValueType::Integer)) return {}; result.values[i] = word; } return result; } ResourcePlan ExtractResourcePlan(const Program& program) { ResourcePlan plan; plan.stage = program.stage; plan.shader_hash = program.shader_hash; plan.user_data_base = program.user_data_base; plan.user_data_count = program.user_data_count; plan.info = program.info; plan.memory_info = program.memory_info; plan.srt_plan_complete = program.srt_plan_complete; plan.resource_tracking_complete = program.resource_tracking_complete; std::unordered_map cloned; std::function Clone = [&](Value value) -> Value { value = value.Resolve(); const auto* source = value.TryInstruction(); if (source == nullptr) { return value; } if (source->GetOpcode() == ValueOpcode::ReadFirstLane && ValidateRuntimeValue(program, source->Arg(0))) { // Uniform values need no new EXEC context; preserve their shared evaluation memo. return Clone(source->Arg(0)); } if (source->GetOpcode() == ValueOpcode::Phi) { const auto invariant = ResolveInvariantPhi(program, value); if (!invariant.IsEmpty() && invariant != value) { return Clone(invariant); } } if (const auto found = cloned.find(source); found != cloned.end()) { return Value(found->second); } if (source->GetOpcode() == ValueOpcode::LoadBufferU32) { plan.requires_specialization_memory = true; plan.capture_specialization_reads = true; } auto& target = plan.value_storage.emplace_back(source->GetOpcode(), source->Flags()); cloned.emplace(source, &target); if (source->GetOpcode() == ValueOpcode::Phi) { for (size_t index = 0; index < source->NumArgs(); index++) { target.AddPhiOperand(nullptr, Clone(source->Arg(index))); } } else { for (size_t index = 0; index < source->NumArgs(); index++) { target.SetArg(index, Clone(source->Arg(index))); } } return Value(&target); }; plan.descriptor_sources.reserve(program.descriptor_sources.size()); for (const auto& source: program.descriptor_sources) { auto& target = plan.descriptor_sources.emplace_back(); target.dword_count = source.dword_count; target.indirect_descriptor = source.indirect_descriptor; if (target.indirect_descriptor.has_value()) { target.indirect_descriptor->key_count = Clone(target.indirect_descriptor->key_count); target.indirect_descriptor->selector_first = Clone(target.indirect_descriptor->selector_first); target.indirect_descriptor->selector_mask = Clone(target.indirect_descriptor->selector_mask); } for (uint32_t dword = 0; dword < source.dword_count; dword++) { target.dwords[dword] = Clone(source.dwords[dword]); } } plan.srt_reads.reserve(program.srt_reads.size()); for (const auto& read: program.srt_reads) { plan.srt_reads.push_back({Clone(read.value), read.flat_offset}); } // A proven uniform factor can decide a branch even when its other lanes are unknown. // Keep only that Boolean structure, never the varying shader dependency graph. Value unknown; std::function ClonePredicate = [&](Value value) -> Value { value = value.Resolve(); if (value.IsEmpty()) return {}; if (ValidateRuntimeValue(program, value, RuntimeValueType::Integer)) return Clone(value); const auto* inst = value.TryInstruction(); if (inst == nullptr) return {}; const auto op = inst->GetOpcode(); if (op == ValueOpcode::ConditionRef || op == ValueOpcode::LogicalNot) { const auto operand = ClonePredicate(inst->Arg(0)); if (operand.IsEmpty() || op == ValueOpcode::ConditionRef) return operand; auto& node = plan.value_storage.emplace_back(op); node.SetArg(0, operand); return Value(&node); } if (op != ValueOpcode::LogicalAnd && op != ValueOpcode::LogicalOr) return {}; auto left = ClonePredicate(inst->Arg(0)); auto right = ClonePredicate(inst->Arg(1)); if (left.IsEmpty() && right.IsEmpty()) return {}; if (left.IsEmpty() || right.IsEmpty()) { if (unknown.IsEmpty()) unknown = Value(&plan.value_storage.emplace_back(ValueOpcode::UndefU1)); if (left.IsEmpty()) left = unknown; if (right.IsEmpty()) right = unknown; } auto& node = plan.value_storage.emplace_back(op); node.SetArg(0, left); node.SetArg(1, right); return Value(&node); }; plan.control_flow = ResourceControlFlow(program, ClonePredicate); plan.uniform_fill = AnalyzeUniformFill(program); for (uint32_t i = 0; i < plan.uniform_fill.fill.words; ++i) { plan.uniform_fill.values[i] = Clone(plan.uniform_fill.values[i]); } plan.clean_flat_slots.resize(plan.srt_reads.size()); bool capture_indirect_reads = false; for (const auto& source: plan.descriptor_sources) { if (!source.indirect_descriptor.has_value()) continue; const auto& indirect = *source.indirect_descriptor; plan.requires_specialization_memory = true; capture_indirect_reads |= indirect.material_source != UINT32_MAX || !indirect.sources.empty(); MarkCleanFlatSlots(plan, Source(plan, indirect.material_source), plan.clean_flat_slots, indirect.selector_mask); MarkCleanFlatSlots(plan, nullptr, plan.clean_flat_slots, indirect.selector_first); MarkCleanFlatSlots(plan, nullptr, plan.clean_flat_slots, indirect.key_count); if (indirect.sources.empty()) { MarkCleanFlatSlots(plan, Source(plan, indirect.table_source), plan.clean_flat_slots); } else { for (const auto candidate: indirect.sources) { MarkCleanFlatSlots(plan, Source(plan, candidate), plan.clean_flat_slots); } } } plan.capture_specialization_reads |= capture_indirect_reads || !plan.control_flow.empty(); if (plan.capture_specialization_reads) { // Clean writable addresses prove that shader writes cannot overlap captured reads. for (const auto& buffer: plan.info.buffers) { if (buffer.written) MarkCleanFlatSlots(plan, Source(plan, buffer.source), plan.clean_flat_slots); } for (const auto& image: plan.info.images) { if (image.written) MarkCleanFlatSlots(plan, Source(plan, image.source), plan.clean_flat_slots); } } if (capture_indirect_reads) plan.resource_tracking_complete &= !program.has_address_writes; return plan; } bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime, ResourceSnapshot& snapshot, ResourceSpecialization& specialization) { if (!program.resource_tracking_complete || (program.requires_specialization_memory && runtime.read_specialization_memory == nullptr)) { return false; } const bool capture_reads = program.capture_specialization_reads; auto& reads = snapshot.specialization_reads; reads.clear(); ReadCapture capture {runtime, reads}; SrtRuntime observed = runtime; if (capture_reads) { observed.userdata = &capture; observed.read_specialization_memory = runtime.read_specialization_memory != nullptr ? CaptureStrictRead : nullptr; observed.read_memory = CaptureOrdinaryRead; } SrtWalker clean(program, CleanRuntime(observed)); SrtWalker walker(program, observed, program.clean_flat_slots, capture_reads || program.requires_specialization_memory ? &clean : nullptr); if (!walker.RefreshFlatBuffer(snapshot.flattened_srt)) { return false; } const auto active = std::span(program.active_sources); snapshot.uniform_fill = {}; const auto& fill = program.uniform_fill; const auto words = fill.fill.words; std::array stored {}; bool uniform_fill = words != 0; for (uint32_t i = 0; i < words && uniform_fill; ++i) { uniform_fill = clean.Evaluate(fill.values[i], stored[i]) && stored[i] == stored[0]; } if (uniform_fill) { snapshot.uniform_fill = fill.fill; snapshot.uniform_fill.value = stored[0]; } const auto evaluate = [&](uint32_t source, DescriptorValue& value, bool written = false) { if (source >= program.descriptor_sources.size()) { return false; } if (active.empty() || active[source]) { return (capture_reads && written ? clean : walker).EvaluateDescriptor(source, value); } value = {}; value.dword_count = program.descriptor_sources[source].dword_count; return true; }; snapshot.buffers.resize(program.info.buffers.size()); specialization.buffers.assign(program.info.buffers.size(), {}); for (uint32_t i = 0; i < snapshot.buffers.size(); ++i) { const auto base_index = i < program.info.buffers.size() ? i : specialization.buffers[i].indirect_root; if (base_index >= program.info.buffers.size()) return false; const auto& base = program.info.buffers[base_index]; if (i < program.info.buffers.size()) { const auto* source = Source(program, base.source); if (source == nullptr) return false; if (source->indirect_descriptor.has_value()) { snapshot.buffers[i] = {.dword_count = 4u}; if (active.empty() || active[base.source]) { if (!MaterializeIndirectDescriptor( program, *source->indirect_descriptor, i, 4u, observed, clean, snapshot, snapshot.buffers, specialization.buffers, ShaderInfo::MaxBuffers, NormalizeIndirectStoreBuffer)) return false; } } else if (!evaluate(base.source, snapshot.buffers[i], base.written)) { return false; } } auto& descriptor_value = snapshot.buffers[i]; ShaderBufferResource descriptor; if (!DecodeBufferDescriptor(descriptor_value, descriptor)) { return SpecializationFail(fmt::format("buffer descriptor {} has invalid width", i)); } if (descriptor.Type() != 0) { descriptor_value.dwords.fill(0); descriptor = {}; } auto packed_stride = descriptor.PackedStride(); const auto stride = packed_stride & 0x3fffu; const bool swizzle = stride != 0u && ((packed_stride >> 14u) & 1u) != 0u; if (stride == 0u) { packed_stride &= ~((1u << 14u) | (3u << 16u)); } else if (!swizzle) { packed_stride &= ~(3u << 16u); } auto& buffer = specialization.buffers[i]; buffer.packed_stride = packed_stride; buffer.descriptor_format = base.formatted ? descriptor.Format() : Prospero::BufferFormat::kInvalid; buffer.descriptor_swizzle = base.formatted ? descriptor.DstSelXYZW() : DstSel(4, 5, 6, 7); buffer.zero_stride_oob = descriptor.OutOfBounds() == 0u && stride == 0u; } snapshot.images.resize(program.info.images.size()); specialization.images.resize(program.info.images.size()); for (uint32_t i = 0; i < program.info.images.size(); ++i) { const auto& image = program.info.images[i]; specialization.images[i] = { .numeric_class = image.numeric_class, .dimension = image.dimension, .mip_count = image.mip_count, .conversion_format = image.conversion_format, .shader_swizzle = image.shader_swizzle, .indirect_root = image.indirect_root, .indirect_mapping_offset = image.indirect_mapping_offset, .indirect_search_iterations = image.indirect_search_iterations, .cube = image.cube, }; const auto* source = Source(program, image.source); if (source == nullptr) { return false; } if (source->indirect_descriptor.has_value()) { snapshot.images[i] = {.dword_count = 8u}; if (!active.empty() && !active[image.source]) { continue; } const auto& indirect = *source->indirect_descriptor; if (!MaterializeIndirectDescriptor( program, indirect, i, 8u, observed, clean, snapshot, snapshot.images, specialization.images, UINT32_MAX, [&](DescriptorValue& value) { if (NullImageDescriptor(value) || !ValidImageDescriptor(value, image.r128)) value.dwords.fill(0u); return true; })) { return false; } } else { if (!evaluate(image.source, snapshot.images[i], image.written)) { return false; } if (!ValidImageDescriptor(snapshot.images[i], image.r128)) { snapshot.images[i].dwords.fill(0); } } } snapshot.samplers.resize(program.info.samplers.size()); for (uint32_t i = 0; i < program.info.samplers.size(); ++i) { if (!evaluate(program.info.samplers[i].source, snapshot.samplers[i])) { return false; } if (program.info.samplers[i].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 // encoded-zero primary and secondary bias; linear primary-mip selection is unsupported. if (filter > 1u || (filter == 1u && (control & 0xfffffu) != 0u)) { return SpecializationFail( "explicit-LOD gather requires mip filtering None or Point with zero LOD biases"); } } } snapshot.user_data.assign(runtime.user_data.begin(), runtime.user_data.end()); return BuildResourceSpecialization(program, snapshot, specialization); } void ApplyResourceSpecialization(Program& program, const ResourceSpecialization& specialization) { EXIT_IF(!program.resource_tracking_complete || program.shader_info_complete || program.binding_layout_complete); EXIT_IF(program.info.buffers.size() > specialization.buffers.size() || program.info.images.size() > specialization.images.size()); auto& buffers = program.info.buffers; const auto original_buffer_count = buffers.size(); buffers.reserve(specialization.buffers.size()); for (uint32_t index = 0; index < specialization.buffers.size(); ++index) { const auto& source = specialization.buffers[index]; if (index >= buffers.size()) { EXIT_IF(source.indirect_root >= original_buffer_count); buffers.push_back(buffers[source.indirect_root]); } auto& buffer = buffers[index]; buffer.packed_stride = source.packed_stride; buffer.descriptor_format = source.descriptor_format; buffer.descriptor_swizzle = source.descriptor_swizzle; buffer.indirect_root = source.indirect_root; buffer.indirect_mapping_offset = source.indirect_mapping_offset; buffer.indirect_search_iterations = source.indirect_search_iterations; buffer.indirect_resources.clear(); } for (uint32_t index = 0; index < buffers.size(); ++index) { const auto root = buffers[index].indirect_root; if (root != BufferResource::NoIndirectBuffer) { EXIT_IF(root >= buffers.size()); buffers[root].indirect_resources.push_back(index); } } auto& images = program.info.images; const auto original_image_count = images.size(); images.reserve(specialization.images.size()); for (uint32_t index = 0; index < specialization.images.size(); index++) { const auto& source = specialization.images[index]; if (index >= images.size()) { EXIT_IF(source.indirect_root >= original_image_count); images.push_back(images[source.indirect_root]); } auto& image = images[index]; image.numeric_class = source.numeric_class; image.dimension = source.dimension; image.mip_count = source.mip_count; image.conversion_format = source.conversion_format; image.shader_swizzle = source.shader_swizzle; image.indirect_root = source.indirect_root; image.indirect_mapping_offset = source.indirect_mapping_offset; image.indirect_search_iterations = source.indirect_search_iterations; image.cube = source.cube; image.indirect_resources.clear(); } for (uint32_t index = 0; index < images.size(); index++) { const auto root = images[index].indirect_root; if (root != ImageResource::NoIndirectImage) { EXIT_IF(root >= images.size()); images[root].indirect_resources.push_back(index); } } SamplerPlan sampler_plan; EXIT_IF(!BuildSamplerPlan(program.info, sampler_plan)); auto& samplers = program.info.samplers; auto& sampled_pairs = program.info.sampled_pairs; const auto original_sampler_count = samplers.size(); samplers.reserve(sampler_plan.sampler_count); for (uint32_t index = 0; index < sampler_plan.sampler_count; index++) { const auto& binding = sampler_plan.bindings[index]; if (index >= original_sampler_count) { samplers.push_back(samplers[binding.source]); } samplers[index].snapshot_index = binding.source; samplers[index].force_point_filtering = binding.type == SamplerClass::PointInteger; samplers[index].integer_border = binding.type != SamplerClass::Float; } for (auto& pair: sampled_pairs) { const auto type = static_cast(ClassifySampler(images[pair.image])); pair.sampler = sampler_plan.mapping[pair.sampler][type]; EXIT_IF(pair.sampler == UINT32_MAX); samplers[pair.sampler].depth_compare |= images[pair.image].depth_compare; } auto& memory_info = program.memory_info; const ImageRemap image_remap(specialization); for (auto* block: program.blocks) { for (auto it = block->begin(); it != block->end(); ++it) { auto& inst = *it; if (inst.GetOpcode() == ValueOpcode::GetImageResource) { inst.SetFlags(image_remap[inst.Flags()]); continue; } if (BufferAccessOf(inst.GetOpcode()) == BufferAccess::Read) { const auto& memory = memory_info[inst.Flags().index]; if (memory.kind == ResourceKind::Buffer && specialization.buffers[memory.resource].zero_stride_oob) { // Bounds mode 0 checks offset >= stride, so zero stride // makes every vector read out of bounds regardless of its address. const auto count = BufferComponentCount(inst.GetOpcode()); std::array values {Value(0u), Value(0u), Value(0u), Value(0u)}; if (memory.formatted && !memory.typed) { const auto& buffer = buffers[memory.resource]; const auto format = Format::GetFormatInfo(buffer.descriptor_format); for (uint32_t component = 0; component < count; component++) { if (format.type == Format::ComponentType::Unknown || GetDstSel(buffer.descriptor_swizzle, component) != 1u) continue; const auto one = Format::FormattedConstantBits( format, Format::FormattedSourceKind::One); values[component] = Value(&*block->PrependNewInst( it, ValueOpcode::SelectU32, {inst.Arg(inst.NumArgs() - 1), Value(one), Value(0u)})); } } Value result = values[0]; switch (inst.GetType()) { case Type::U8: result = Value(uint8_t {0}); break; case Type::U16: result = Value(uint16_t {0}); break; case Type::U32x2: result = Value(&*block->PrependNewInst(it, ValueOpcode::CompositeConstructU32x2, {values[0], values[1]})); break; case Type::U32x3: result = Value(&*block->PrependNewInst(it, ValueOpcode::CompositeConstructU32x3, {values[0], values[1], values[2]})); break; case Type::U32x4: result = Value(&*block->PrependNewInst(it, ValueOpcode::CompositeConstructU32x4, {values[0], values[1], values[2], values[3]})); break; default: break; } inst.ReplaceUsesWith(result); } continue; } const auto image_opcode = ImageOpcodeInfoOf(inst.GetOpcode()); if (image_opcode.access == ImageAccess::None) { continue; } const auto index = inst.Flags().index; EXIT_IF(index >= memory_info.size()); auto& memory = memory_info[index]; EXIT_IF(memory.resource >= images.size()); const auto& image = images[memory.resource]; if (specialization.images[memory.resource].fmask) { EXIT_IF(inst.GetOpcode() != ValueOpcode::ImageRead || memory.data_bits != 32u); // Vulkan MSAA stores each sample directly; FMASK's four-bit fragment indices // therefore map each coverage sample to the same host sample. constexpr uint32_t indices[] = {0x76543210u, 0xfedcba98u}; std::array fragments; for (uint32_t component = 0; component < fragments.size(); component++) { const auto selected = block->PrependNewInst( it, ValueOpcode::SelectU32, {inst.Arg(2), Value(indices[component]), Value(0u)}); fragments[component] = Value(&*selected); } const auto result = block->PrependNewInst( it, ValueOpcode::CompositeConstructU32x4, {fragments[0], fragments[1], Value(0u), Value(0u)}); inst.ReplaceUsesWith(Value(&*result)); continue; } if (image_opcode.needs_sampler && memory.sampler < original_sampler_count) { const auto type = static_cast(ClassifySampler(image)); memory.sampler = sampler_plan.mapping[memory.sampler][type]; EXIT_IF(memory.sampler == UINT32_MAX); } EXIT_IF(image.indirect_root == memory.resource && inst.GetOpcode() != ValueOpcode::ImageSampleRaw); } } for (auto& memory: memory_info) { if (memory.kind == ResourceKind::Image && !memory.planning_only) { memory.resource = image_remap[memory.resource]; } } for (auto& buffer: buffers) { if (buffer.image_alias != BufferResource::NoImageAlias) { buffer.image_alias = image_remap[buffer.image_alias]; } } for (auto& pair: sampled_pairs) { pair.image = image_remap[pair.image]; } for (auto& image: images) { if (image.indirect_root != ImageResource::NoIndirectImage) { image.indirect_root = image_remap[image.indirect_root]; } for (auto& resource: image.indirect_resources) { resource = image_remap[resource]; } } image_remap.Apply(images); } } // namespace Libs::Graphics::ShaderRecompiler::IR