#include "graphics/shader/recompiler/ir/passes/ResourceTracking.h" #include "common/assert.h" #include "graphics/shader/recompiler/ir/ShaderIR.h" #include "graphics/shader/recompiler/ir/passes/SrtWalker.h" #include #include #include #include namespace Libs::Graphics::ShaderRecompiler::IR { namespace { constexpr uint32_t SamplerBorderClampMask = (1u << 2u) | (1u << 5u) | (1u << 8u); constexpr uint32_t SamplerDword3ReservedMask = 0x3ffff000u; uint32_t PossibleU32Bits(Value value) { value = value.Resolve(); if (value.IsImmediate()) { return value.GetType() == Type::U32 ? value.U32() : UINT32_MAX; } const auto* inst = value.TryInstruction(); if (inst == nullptr) { return UINT32_MAX; } switch (inst->GetOpcode()) { case ValueOpcode::BitwiseAnd32: return PossibleU32Bits(inst->Arg(0)) & PossibleU32Bits(inst->Arg(1)); case ValueOpcode::BitwiseOr32: return PossibleU32Bits(inst->Arg(0)) | PossibleU32Bits(inst->Arg(1)); case ValueOpcode::ShiftLeftLogical32: { const auto shift = inst->Arg(1).Resolve(); return shift.IsImmediate() && shift.GetType() == Type::U32 ? PossibleU32Bits(inst->Arg(0)) << (shift.U32() & 31u) : UINT32_MAX; } default: return UINT32_MAX; } } Value CanonicalizeSampleAdjustDword3(Value value) { for (;;) { value = value.Resolve(); const auto* inst = value.TryInstruction(); if (inst == nullptr || inst->GetOpcode() != ValueOpcode::BitwiseOr32) { return value; } const auto left = inst->Arg(0).Resolve(); const auto right = inst->Arg(1).Resolve(); const bool left_reserved = (PossibleU32Bits(left) & ~SamplerDword3ReservedMask) == 0; const bool right_reserved = (PossibleU32Bits(right) & ~SamplerDword3ReservedMask) == 0; if (left_reserved && right_reserved) { return Value(0u); } if (left_reserved) { value = right; } else if (right_reserved) { value = left; } else { return value; } } } const char* StageName(ShaderType stage) { switch (stage) { case ShaderType::Vertex: return "vertex"; case ShaderType::Pixel: return "pixel"; case ShaderType::Fetch: return "fetch"; case ShaderType::Compute: return "compute"; default: return "unknown"; } } uint32_t ByteExtent(const MemoryInfo& memory) { const auto bytes = std::max((memory.data_bits + 7u) / 8u, 1u); const auto count = std::max(memory.data_dwords, 1u); const auto end = static_cast(memory.offset) + static_cast(bytes) * count; return end > UINT32_MAX ? UINT32_MAX : static_cast(end); } class Tracker { public: explicit Tracker(Program& program): m_program(program), m_info(program.info) { m_info.buffers.clear(); m_info.images.clear(); m_info.samplers.clear(); m_info.sampled_pairs.clear(); m_info.uses_dma = false; m_shader_writes = HasShaderMemoryWrites(program); } void Run() { if (m_program.resource_tracking_complete) { Fail(0, "resources already tracked"); } if (!m_program.srt_plan_complete) { Fail(0, "SRT plan is not ready"); } PlanIndirectImages(); for (auto* block: m_program.blocks) { for (auto& inst: *block) { Collect(inst); } } LinkImageAliases(); for (const auto& patch: m_handle_patches) { patch.handle->SetFlags(patch.resource); } for (const auto& patch: m_memory_patches) { auto& memory = m_program.memory_info[patch.index]; memory.resource = patch.resource; if (patch.has_sampler) { memory.sampler = patch.sampler; } } for (const auto& plan: m_indirect_images) { plan.handle->SetArg(0, plan.key); for (uint32_t dword = 0; dword < 4u; dword++) { plan.handle->SetArg(dword + 1u, plan.roots[dword + 4u]); } for (uint32_t dword = 5u; dword < plan.roots.size(); dword++) { plan.handle->SetArg(dword, plan.key); } for (const auto index: plan.memory) { m_program.memory_info[index].planning_only = true; } } std::erase_if(m_program.dynamic_reads, [&](Value value) { const auto* inst = value.Resolve().TryInstruction(); return std::any_of(m_indirect_images.begin(), m_indirect_images.end(), [&](const IndirectImagePlan& plan) { return std::ranges::find(plan.reads, inst) != plan.reads.end(); }); }); m_program.descriptor_sources = std::move(m_sources); m_program.info = std::move(m_info); m_program.resource_tracking_complete = true; } private: struct HandlePatch { Inst* handle = nullptr; uint32_t resource = 0; }; struct MemoryPatch { uint32_t index = 0; uint32_t resource = 0; uint32_t sampler = 0; bool has_sampler = false; }; struct IndirectImagePlan { Inst* handle = nullptr; uint32_t source = 0; Value key; std::array roots {}; std::array memory {}; std::array reads {}; }; [[noreturn]] void Fail(uint32_t pc, const std::string& reason) const { const auto message = fmt::format("shader resource tracking: hash=0x{:016x} stage={} pc=0x{:08x} {}", m_program.shader_hash, StageName(m_program.stage), pc, reason); EXIT("%s", message.c_str()); std::abort(); } Value LowerDescriptorPhi(Value value, const Block* use) { value = value.Resolve(); const auto* phi = value.TryInstruction(); if (m_shader_writes || phi == nullptr || phi->GetOpcode() != ValueOpcode::Phi || phi->NumArgs() != 2u || phi->NumPhiBlocks() != 2u || phi->GetType() != Type::U32 || m_program.blocks.size() != m_program.block_info.size()) { return value; } const auto* merge = phi->Parent(); const auto* branch = phi->PhiBlock(0); if (merge == nullptr || branch == nullptr || phi->PhiBlock(1) == nullptr || branch == phi->PhiBlock(1)) { return value; } // Structurization can merge the descriptor and its use predicate in parallel Phis. // Match their incoming blocks to exclude only edges that cannot reach this use. if (use != nullptr && use->ImmPredecessors().size() == 1u && use->ImmPredecessors()[0] == merge) { const auto merge_it = std::ranges::find(m_program.blocks, merge); const auto use_it = std::ranges::find(m_program.blocks, use); if (merge_it != m_program.blocks.end() && use_it != m_program.blocks.end()) { const auto& info = m_program.block_info[merge_it - m_program.blocks.begin()]; const auto& term = info.terminator; const auto id = m_program.block_info[use_it - m_program.blocks.begin()].id; const auto* condition = info.condition.Resolve().TryInstruction(); if (term.kind == CFG::TerminatorKind::ConditionalBranch && term.true_block != term.false_block && (id == term.true_block || id == term.false_block) && condition != nullptr && condition->GetOpcode() == ValueOpcode::Phi && condition->GetType() == Type::U1 && condition->Parent() == merge && condition->NumArgs() == 2u && condition->NumPhiBlocks() == 2u) { const bool taken = id == term.true_block; for (uint32_t skipped = 0; skipped < 2u; skipped++) { const auto excluded = condition->Arg(skipped).Resolve(); const auto included = condition->Arg(skipped ^ 1u).Resolve(); if (!excluded.IsImmediate() || excluded.GetType() != Type::U1 || excluded.U1() == taken || (included.IsImmediate() && included.U1() != taken)) { continue; } for (uint32_t selected = 0; selected < 2u; selected++) { if (phi->PhiBlock(selected) == condition->PhiBlock(skipped ^ 1u) && phi->PhiBlock(selected ^ 1u) == condition->PhiBlock(skipped)) { return phi->Arg(selected); } } } } } } for (const auto& [original, selected]: m_descriptor_selections) { if (original == phi) { return selected; } } if (branch->ImmSuccessors().size() != 2u) { if (branch->ImmPredecessors().size() != 1u) { return value; } branch = branch->ImmPredecessors()[0]; } if (branch == merge || branch->ImmSuccessors().size() != 2u) { return value; } std::array target_ids; for (uint32_t arm = 0; arm < 2; arm++) { const auto* incoming = phi->PhiBlock(arm); if (incoming == merge || (incoming != branch && (incoming->ImmPredecessors().size() != 1u || incoming->ImmPredecessors()[0] != branch || incoming->ImmSuccessors().size() != 1u || incoming->ImmSuccessors()[0] != merge))) { return value; } const auto* target = incoming == branch ? merge : incoming; const auto it = std::ranges::find(m_program.blocks, target); if (it == m_program.blocks.end()) { return value; } target_ids[arm] = m_program.block_info[it - m_program.blocks.begin()].id; } const auto branch_it = std::ranges::find(m_program.blocks, branch); if (branch_it == m_program.blocks.end()) { return value; } const auto& info = m_program.block_info[branch_it - m_program.blocks.begin()]; const auto& term = info.terminator; if (term.kind != CFG::TerminatorKind::ConditionalBranch || !((term.true_block == target_ids[0] && term.false_block == target_ids[1]) || (term.false_block == target_ids[0] && term.true_block == target_ids[1])) || !ValidateRuntimeValue(m_program, info.condition, RuntimeValueType::Integer) || !ValidateRuntimeValue(m_program, phi->Arg(0)) || !ValidateRuntimeValue(m_program, phi->Arg(1))) { return value; } // Retain a host expression; replacing the GPU Phi would break SSA dominance. const auto true_arg = term.true_block == target_ids[0] ? 0u : 1u; auto& selected = m_program.value_storage.emplace_back(ValueOpcode::SelectU32); selected.SetArg(0, info.condition); selected.SetArg(1, phi->Arg(true_arg)); selected.SetArg(2, phi->Arg(true_arg ^ 1u)); m_descriptor_selections.emplace_back(phi, Value(&selected)); return Value(&selected); } void MakeSource(const Inst& handle, uint32_t width, bool sampler, bool sample_adjust, DescriptorSource& descriptor, uint32_t pc) { if (handle.NumArgs() != width) { Fail(pc, fmt::format("{} has {} descriptor dwords, expected {}", ValueOpcodeName(handle.GetOpcode()), handle.NumArgs(), width)); } descriptor.dword_count = width; for (uint32_t i = 0; i < width; i++) { descriptor.dwords[i] = LowerDescriptorPhi(handle.Arg(i), handle.Parent()); } if (sample_adjust) { descriptor.dwords[3] = CanonicalizeSampleAdjustDword3(descriptor.dwords[3]); } const auto dword0 = descriptor.dwords[0].Resolve(); if (sampler && dword0.IsImmediate() && dword0.GetType() == Type::U32 && (dword0.U32() & SamplerBorderClampMask) == 0) { // Border color and its table index are unused unless a clamp axis selects border mode. descriptor.dwords[3] = Value(0u); } } bool ValidateSource(const DescriptorSource& descriptor, uint32_t& bad_dword) const { for (uint32_t i = 0; i < descriptor.dword_count; i++) { bad_dword = i; if (descriptor.dwords[i].Resolve().GetType() != Type::U32) { return false; } if (!ValidateRuntimeValue(m_program, descriptor.dwords[i])) { return false; } } return true; } uint32_t InternSource(const DescriptorSource& descriptor) { for (uint32_t candidate = 0; candidate < m_sources.size(); candidate++) { const auto& current = m_sources[candidate]; if (current.dword_count != descriptor.dword_count || current.indirect_image.has_value() != descriptor.indirect_image.has_value()) { continue; } if (current.indirect_image.has_value()) { const auto& a = *current.indirect_image; const auto& b = *descriptor.indirect_image; 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.table_offset != b.table_offset || !EquivalentValue(m_program, a.key_count, b.key_count)) continue; } bool same = true; for (uint32_t i = 0; i < descriptor.dword_count; i++) { same = same && EquivalentValue(m_program, current.dwords[i], descriptor.dwords[i]); } if (same) { return candidate; } } m_sources.push_back(descriptor); return static_cast(m_sources.size() - 1); } static bool ImmediateU32(Value value, uint32_t& result) { value = value.Resolve(); if (!value.IsImmediate() || value.GetType() != Type::U32) { return false; } result = value.U32(); return true; } static bool UsesOnly(const Inst& value, std::span users) { return !value.Uses().empty() && std::ranges::all_of(value.Uses(), [&](const Use& use) { return std::ranges::find(users, use.user) != users.end(); }); } const MemoryInfo* ScalarReadMemory(const Inst& read, uint32_t& index) const { const bool address = read.GetOpcode() == ValueOpcode::LoadAddressU32; if (!(address ? read.NumArgs() == 4u : read.GetOpcode() == ValueOpcode::ReadConstBuffer && read.NumArgs() == 2u)) { return nullptr; } if (address) { const auto high = read.Arg(2).Resolve(); const auto enabled = read.Arg(3).Resolve(); if (!high.IsImmediate() || high.GetType() != Type::U32 || high.U32() != 0u || !enabled.IsImmediate() || enabled.GetType() != Type::U1 || !enabled.U1()) { return nullptr; } } index = read.Flags().index; if (index >= m_program.memory_info.size()) { return nullptr; } const auto& memory = m_program.memory_info[index]; return memory.kind == (address ? ResourceKind::ScalarAddress : ResourceKind::ScalarBuffer) && memory.data_bits == 32u && memory.data_dwords == 1u ? &memory : nullptr; } bool MemoryIndexBelongsTo(uint32_t index, const Inst& owner) const { for (const auto* block: m_program.blocks) { for (const auto& inst: *block) { const auto op = inst.GetOpcode(); if ((BufferAccessOf(op) == BufferAccess::None && AddressOpcodeInfoOf(op).access == AddressAccess::None && ImageOpcodeInfoOf(op).access == ImageAccess::None) || &inst == &owner) { continue; } if (inst.Flags().index == index) { return false; } } } return true; } bool MakeRuntimeTableSource(const Inst& handle, uint32_t pc, DescriptorSource& descriptor) { const auto width = handle.GetOpcode() == ValueOpcode::GetBufferResource ? 4u : handle.GetOpcode() == ValueOpcode::GetAddressResource ? 2u : 0u; if (width == 0u) { return false; } MakeSource(handle, width, false, false, descriptor, pc); uint32_t bad_dword = 0; return ValidateSource(descriptor, bad_dword); } bool MatchMaterialOffset(Value value, Value& selector, uint32_t& stride, uint32_t& offset) const { value = value.Resolve(); offset = 0; auto* candidate = value.TryInstruction(); if (candidate != nullptr && candidate->GetOpcode() == ValueOpcode::IAdd32 && candidate->NumArgs() == 2u) { uint32_t immediate = 0; if (ImmediateU32(candidate->Arg(0), immediate)) { value = candidate->Arg(1).Resolve(); } else if (ImmediateU32(candidate->Arg(1), immediate)) { value = candidate->Arg(0).Resolve(); } else { return false; } offset = immediate; } const auto* multiply = value.TryInstruction(); if (multiply == nullptr || multiply->GetOpcode() != ValueOpcode::IMul32 || multiply->NumArgs() != 2u) { return false; } if (ImmediateU32(multiply->Arg(0), stride)) { selector = multiply->Arg(1).Resolve(); } else if (ImmediateU32(multiply->Arg(1), stride)) { selector = multiply->Arg(0).Resolve(); } else { return false; } const auto* selector_inst = selector.TryInstruction(); return stride != 0u && selector_inst != nullptr && selector_inst->GetOpcode() == ValueOpcode::ReadFirstLane; } bool NonzeroOnEntry(Value value, const Block* block) const { if (m_program.blocks.size() != m_program.block_info.size()) { return false; } // Each unique predecessor must execute before this use. Stop at joins: an // unrelated comparison is not a bound on FindILsb's zero-input sentinel. for (size_t depth = 0; block != nullptr && depth < m_program.blocks.size(); ++depth) { if (block->ImmPredecessors().size() != 1u) { return false; } const auto* previous = block->ImmPredecessors()[0]; const auto current_it = std::ranges::find(m_program.blocks, block); const auto previous_it = std::ranges::find(m_program.blocks, previous); if (current_it == m_program.blocks.end() || previous_it == m_program.blocks.end()) { return false; } const auto& info = m_program.block_info[previous_it - m_program.blocks.begin()]; const auto id = m_program.block_info[current_it - m_program.blocks.begin()].id; const auto& term = info.terminator; if (term.kind == CFG::TerminatorKind::ConditionalBranch && (term.true_block == id) != (term.false_block == id)) { bool positive = term.true_block == id; const auto* test = info.condition.Resolve().TryInstruction(); while (test != nullptr && test->GetOpcode() == ValueOpcode::LogicalNot) { positive = !positive; test = test->Arg(0).Resolve().TryInstruction(); } if (test != nullptr && test->NumArgs() == 2u && ((test->GetOpcode() == ValueOpcode::INotEqual32 && positive) || (test->GetOpcode() == ValueOpcode::IEqual32 && !positive))) { for (uint32_t arg = 0; arg < 2u; ++arg) { uint32_t immediate; if (ImmediateU32(test->Arg(arg), immediate) && immediate == 0u && EquivalentValue(m_program, test->Arg(arg ^ 1u), value)) { return true; } } } } block = previous; } return false; } bool MatchTableOffset(Value value, Value& key, uint32_t& offset) const { offset = 0; for (;;) { const auto* inst = value.Resolve().TryInstruction(); if (inst == nullptr || inst->NumArgs() != 2u) { return false; } uint32_t immediate; if (inst->GetOpcode() == ValueOpcode::ShiftLeftLogical32 && ImmediateU32(inst->Arg(1), immediate) && immediate == 5u) { key = inst->Arg(0).Resolve(); return key.GetType() == Type::U32; } if (inst->GetOpcode() != ValueOpcode::IAdd32) { return false; } if (ImmediateU32(inst->Arg(0), immediate)) { value = inst->Arg(1); } else if (ImmediateU32(inst->Arg(1), immediate)) { value = inst->Arg(0); } else { return false; } // These additions are shader U32 arithmetic, before the scalar memory offset. offset += immediate; } } const Block* FindBlock(uint32_t id) const { const auto info = std::ranges::find(m_program.block_info, id, &BlockInfo::id); return info == m_program.block_info.end() ? nullptr : m_program.blocks[info - m_program.block_info.begin()]; } bool CanReach(const Block* start, const Block* target, const Block* avoid) const { std::vector pending {start}; std::vector visited; while (!pending.empty()) { const auto* block = pending.back(); pending.pop_back(); if (block == nullptr || block == avoid || std::ranges::find(visited, block) != visited.end()) continue; if (block == target) return true; visited.push_back(block); for (const auto* next: block->ImmSuccessors()) pending.push_back(next); } return false; } Value BoundedLoopCount(Value key, const Block* use) const { const auto* phi = key.Resolve().TryInstruction(); if (m_shader_writes || phi == nullptr || phi->GetOpcode() != ValueOpcode::Phi || phi->GetType() != Type::U32 || phi->NumArgs() != 2u || m_program.blocks.size() != m_program.block_info.size()) { return {}; } bool induction = false; for (uint32_t initial = 0; initial < 2u; ++initial) { const auto zero = phi->Arg(initial).Resolve(); const auto* step = phi->Arg(initial ^ 1u).Resolve().TryInstruction(); if (!zero.IsImmediate() || zero.GetType() != Type::U32 || zero.U32() != 0u || step == nullptr || step->GetOpcode() != ValueOpcode::IAdd32 || step->Parent() != phi->PhiBlock(initial ^ 1u)) { continue; } uint32_t increment = 0; induction = (step->Arg(0).Resolve() == key && ImmediateU32(step->Arg(1), increment) && increment == 1u) || (step->Arg(1).Resolve() == key && ImmediateU32(step->Arg(0), increment) && increment == 1u); if (induction) break; } if (!induction) return {}; const auto contains = [&](auto&& self, Value value, const Inst* comparison) -> bool { value = value.Resolve(); if (value.TryInstruction() == comparison) return true; const auto* inst = value.TryInstruction(); return inst != nullptr && inst->GetOpcode() == ValueOpcode::LogicalAnd && (self(self, inst->Arg(0), comparison) || self(self, inst->Arg(1), comparison)); }; for (const auto& use_of_key: phi->Uses()) { const auto* compare = use_of_key.user; if (compare->GetOpcode() != ValueOpcode::SLessThan32 || use_of_key.operand != 0u || !ValidateRuntimeValue(m_program, compare->Arg(1))) continue; for (uint32_t i = 0; i < m_program.block_info.size(); ++i) { const auto& info = m_program.block_info[i]; const auto* negated = info.condition.Resolve().TryInstruction(); if (info.terminator.kind != CFG::TerminatorKind::ConditionalBranch || compare->Parent() != m_program.blocks[i] || negated == nullptr || negated->GetOpcode() != ValueOpcode::LogicalNot || !contains(contains, negated->Arg(0), compare) || use == m_program.blocks[i] || CanReach(m_program.blocks.front(), use, m_program.blocks[i]) || CanReach(phi->Parent(), use, m_program.blocks[i]) || CanReach(FindBlock(info.terminator.true_block), use, phi->Parent()) || !CanReach(FindBlock(info.terminator.false_block), use, phi->Parent())) { continue; } return compare->Arg(1); } } return {}; } bool TryMakeIndirectImage(Inst& handle, uint32_t pc, IndirectImagePlan& plan) { if (handle.GetOpcode() != ValueOpcode::GetImageResource || handle.NumArgs() != 8u) { return false; } Inst* table_handle = nullptr; Value key; uint32_t table_offset = 0; for (uint32_t dword = 0; dword < plan.reads.size(); ++dword) { auto* read = handle.Arg(dword).Resolve().TryInstruction(); if (read == nullptr) { return false; } uint32_t memory_index = 0; const auto* memory = ScalarReadMemory(*read, memory_index); if (memory == nullptr || memory->offset > INT32_MAX || (memory->offset & 3u) != 0u || !MemoryIndexBelongsTo(memory_index, *read)) { return false; } auto* current_handle = read->Arg(0).Resolve().TryInstruction(); Value current_key; uint32_t offset = 0; if (current_handle == nullptr || current_handle->GetOpcode() != (memory->kind == ResourceKind::ScalarAddress ? ValueOpcode::GetAddressResource : ValueOpcode::GetBufferResource) || (memory->kind == ResourceKind::ScalarAddress && read->Parent() != handle.Parent()) || (table_handle != nullptr && !EquivalentValue(m_program, Value(table_handle), Value(current_handle))) || !MatchTableOffset(read->Arg(1), current_key, offset) || memory->offset > UINT32_MAX - offset) { return false; } offset += memory->offset; if (dword == 0u) { key = current_key; table_offset = offset; } else if (!EquivalentValue(m_program, key, current_key) || static_cast(table_offset) + dword * sizeof(uint32_t) != offset) { return false; } table_handle = current_handle; const std::array image_users {&handle}; if (!UsesOnly(*read, image_users)) { return false; } plan.memory[dword] = memory_index; plan.reads[dword] = read; } DescriptorSource table_source; if (!MakeRuntimeTableSource(*table_handle, pc, table_source)) { return false; } DescriptorSource material_source; DescriptorSource::IndirectImage indirect; indirect.table_offset = table_offset; if (table_source.dword_count == 2u) { const auto* selector = key.Resolve().TryInstruction(); const bool bitscan = selector != nullptr && selector->GetOpcode() == ValueOpcode::FindILsb32 && selector->NumArgs() == 1u && !m_shader_writes && NonzeroOnEntry(selector->Arg(0), handle.Parent()); if (bitscan) { indirect.key_count = Value(32u); } else { indirect.key_count = BoundedLoopCount(key, handle.Parent()); } if (indirect.key_count.IsEmpty() || (table_offset & 3u) != 0u || (bitscan && table_offset > UINT32_MAX - (32u * 32u - 1u))) return false; } else { auto* material_read = key.Resolve().TryInstruction(); uint32_t material_memory_index = 0; const auto* memory = material_read != nullptr ? ScalarReadMemory(*material_read, material_memory_index) : nullptr; if (table_offset != 0u || memory == nullptr || memory->kind != ResourceKind::ScalarBuffer || memory->offset != 0u || !MemoryIndexBelongsTo(material_memory_index, *material_read)) { return false; } Value selector; if (!MatchMaterialOffset(material_read->Arg(1), selector, indirect.selector_stride, indirect.selector_offset)) { return false; } const auto* shift = plan.reads[0]->Arg(1).Resolve().TryInstruction(); const std::array material_users {shift}; if (!UsesOnly(*material_read, material_users) || !UsesOnly(*shift, plan.reads)) { return false; } const auto* material_handle = material_read->Arg(0).Resolve().TryInstruction(); if (material_handle == nullptr || material_handle->GetOpcode() != ValueOpcode::GetBufferResource || !MakeRuntimeTableSource(*material_handle, pc, material_source)) { return false; } indirect.material_source = InternSource(material_source); } indirect.table_source = InternSource(table_source); DescriptorSource image_source; image_source.dword_count = 8u; image_source.dwords.fill(Value(0u)); std::copy_n(material_source.dwords.begin(), material_source.dword_count, image_source.dwords.begin()); std::copy_n(table_source.dwords.begin(), table_source.dword_count, image_source.dwords.begin() + 4u); image_source.indirect_image = indirect; plan.handle = &handle; plan.source = InternSource(image_source); plan.key = key; plan.roots = image_source.dwords; return true; } const IndirectImagePlan* FindIndirectImage(const Inst& handle) const { const auto found = std::find_if(m_indirect_images.begin(), m_indirect_images.end(), [&](const IndirectImagePlan& plan) { return plan.handle == &handle; }); return found == m_indirect_images.end() ? nullptr : &*found; } bool IsIndirectPlanningMemory(uint32_t index) const { return std::any_of(m_indirect_images.begin(), m_indirect_images.end(), [&](const IndirectImagePlan& plan) { return std::ranges::find(plan.memory, index) != plan.memory.end(); }); } void PlanIndirectImages() { for (auto* block: m_program.blocks) { for (auto& inst: *block) { if (ImageOpcodeInfoOf(inst.GetOpcode()).access == ImageAccess::None || inst.NumArgs() == 0u) { continue; } auto* handle = inst.Arg(0).Resolve().TryInstruction(); if (handle == nullptr || FindIndirectImage(*handle) != nullptr) { continue; } IndirectImagePlan plan; if (TryMakeIndirectImage(*handle, inst.Flags().pc, plan)) { m_indirect_images.push_back(std::move(plan)); } } } } bool GetHandle(Value value, ValueOpcode expected, uint32_t width, uint32_t pc, Inst*& handle, uint32_t& source, bool sampler = false, bool sample_adjust = false) { handle = value.Resolve().TryInstruction(); if (handle == nullptr || handle->GetOpcode() != expected) { Fail(pc, fmt::format("memory operation requires {}", ValueOpcodeName(expected))); } DescriptorSource descriptor; MakeSource(*handle, width, sampler, sample_adjust, descriptor, pc); uint32_t bad_dword = 0; if (!ValidateSource(descriptor, bad_dword)) { if (expected == ValueOpcode::GetBufferResource && std::all_of(descriptor.dwords.begin(), descriptor.dwords.begin() + width, [](Value word) { return word.Resolve().GetType() == Type::U32; })) { return false; } Fail(pc, fmt::format("{} dword {} is not a valid runtime value", ValueOpcodeName(expected), bad_dword)); } source = InternSource(descriptor); return true; } void ValidateAddressHandle(Value value, uint32_t pc) const { const auto* handle = value.Resolve().TryInstruction(); if (handle == nullptr || handle->GetOpcode() != ValueOpcode::GetAddressResource) { Fail(pc, "address operation requires GetAddressResource"); } if (handle->NumArgs() != 2) { Fail(pc, "GetAddressResource must have two address dwords"); } } uint32_t AddBuffer(uint32_t source, const MemoryInfo& memory, ValueOpcode op, uint32_t pc) { for (uint32_t i = 0; i < m_info.buffers.size(); i++) { if (m_info.buffers[i].source == source) { Merge(m_info.buffers[i], memory, op, pc); return i; } } if (m_info.buffers.size() >= ShaderInfo::MaxBuffers) { return UINT32_MAX; } BufferResource resource; resource.source = source; resource.first_use_pc = pc; Merge(resource, memory, op, pc); m_info.buffers.push_back(resource); return static_cast(m_info.buffers.size() - 1); } static void Merge(BufferResource& resource, const MemoryInfo& memory, ValueOpcode op, uint32_t pc) { const auto access = BufferAccessOf(op); const bool atomic = access == BufferAccess::Atomic; const bool write = access == BufferAccess::Write || atomic; resource.first_use_pc = std::min(resource.first_use_pc, pc); resource.max_byte_extent = std::max(resource.max_byte_extent, ByteExtent(memory)); resource.read = resource.read || !write || atomic; resource.written = resource.written || write; resource.atomic = resource.atomic || atomic; resource.formatted = resource.formatted || memory.formatted; resource.scalar = resource.scalar || op == ValueOpcode::ReadConstBuffer || memory.kind == ResourceKind::ScalarBuffer; } uint32_t AddImage(uint32_t source, const MemoryInfo& memory, ValueOpcode op, uint32_t pc) { const auto resource_class = ImageOpcodeInfoOf(op).resource_class; const auto mip = resource_class == ImageResourceClass::Storage && memory.image_has_mip ? ImageMipMode::DynamicStorage : ImageMipMode::None; const bool depth = (memory.image_sample_flags & Decoder::ImageSampleFlagCompare) != 0; for (uint32_t i = 0; i < m_info.images.size(); i++) { auto& image = m_info.images[i]; if (image.source == source && image.resource_class == resource_class && image.dimension == memory.image_dimension && image.mip_mode == mip && image.depth_compare == depth && image.r128 == memory.image_r128) { Merge(image, op, pc); return i; } } if (m_info.images.size() >= ShaderInfo::MaxImages) { return UINT32_MAX; } ImageResource image; image.source = source; image.first_use_pc = pc; image.resource_class = resource_class; image.dimension = memory.image_dimension; image.mip_mode = mip; image.depth_compare = depth; image.r128 = memory.image_r128; Merge(image, op, pc); m_info.images.push_back(image); return static_cast(m_info.images.size() - 1); } static void Merge(ImageResource& image, ValueOpcode op, uint32_t pc) { const auto access = ImageOpcodeInfoOf(op).access; const bool atomic = access == ImageAccess::Atomic; const bool write = access == ImageAccess::Write || atomic; image.first_use_pc = std::min(image.first_use_pc, pc); image.read = image.read || !write || atomic; image.written = image.written || write; image.atomic = image.atomic || atomic; } uint32_t AddSampler(uint32_t source, uint32_t pc) { for (uint32_t i = 0; i < m_info.samplers.size(); i++) { if (m_info.samplers[i].source == source) { m_info.samplers[i].first_use_pc = std::min(m_info.samplers[i].first_use_pc, pc); return i; } } if (m_info.samplers.size() >= ShaderInfo::MaxSamplers) { return UINT32_MAX; } m_info.samplers.push_back({source, pc}); return static_cast(m_info.samplers.size() - 1); } void AddSampledPair(uint32_t image, uint32_t sampler, uint32_t pc) { for (auto& pair: m_info.sampled_pairs) { if (pair.image == image && pair.sampler == sampler) { pair.first_use_pc = std::min(pair.first_use_pc, pc); return; } } if (m_info.sampled_pairs.size() >= ShaderInfo::MaxSampledPairs) { Fail(pc, "sampled image/sampler pair limit exceeded"); } m_info.sampled_pairs.push_back({image, sampler, pc}); } void AddHandlePatch(Inst* handle, uint32_t resource, uint32_t pc) { for (const auto& patch: m_handle_patches) { if (patch.handle == handle) { if (patch.resource != resource) { Fail(pc, fmt::format("{} is reused with incompatible resource classes", ValueOpcodeName(handle->GetOpcode()))); } return; } } m_handle_patches.push_back({handle, resource}); } void AddMemoryPatch(uint32_t index, uint32_t resource, uint32_t sampler, bool has_sampler, uint32_t pc) { for (auto& patch: m_memory_patches) { if (patch.index != index) { continue; } if (patch.resource != resource || (has_sampler && patch.has_sampler && patch.sampler != sampler)) { Fail(pc, "memory metadata is reused with incompatible resources"); } if (has_sampler) { patch.sampler = sampler; patch.has_sampler = true; } return; } m_memory_patches.push_back({index, resource, sampler, has_sampler}); } void Collect(Inst& inst) { const auto op = inst.GetOpcode(); const auto buffer = BufferAccessOf(op); const auto address_info = AddressOpcodeInfoOf(op); const auto image_info = ImageOpcodeInfoOf(op); if (buffer == BufferAccess::None && address_info.access == AddressAccess::None && image_info.access == ImageAccess::None) { return; } const auto flags = inst.Flags(); if (flags.index >= m_program.memory_info.size()) { Fail(flags.pc, fmt::format("memory metadata index {} is out of range", flags.index)); } if (inst.NumArgs() == 0) { Fail(flags.pc, "memory operation has no resource handle"); } const auto& memory = m_program.memory_info[flags.index]; if (memory.planning_only || IsIndirectPlanningMemory(flags.index)) { return; } Inst* handle = nullptr; uint32_t source = 0; uint32_t resource = 0; if (buffer != BufferAccess::None) { if (!GetHandle(inst.Arg(0), ValueOpcode::GetBufferResource, 4, flags.pc, handle, source)) { if (memory.kind != ResourceKind::Buffer || memory.formatted || memory.typed || (op != ValueOpcode::LoadBufferU32x2 && op != ValueOpcode::LoadBufferU32x4)) { Fail(flags.pc, "buffer descriptor is not a valid runtime value; GPU-selected access " "requires a raw DWORD x2/x4 load"); } m_program.memory_info[flags.index].kind = ResourceKind::IndirectBuffer; m_info.uses_dma = true; return; } resource = AddBuffer(source, memory, op, flags.pc); if (resource == UINT32_MAX) { Fail(flags.pc, "buffer resource limit exceeded"); } AddHandlePatch(handle, resource, flags.pc); AddMemoryPatch(flags.index, resource, 0, false, flags.pc); return; } if (address_info.access != AddressAccess::None) { if (!IsAddressResourceKind(memory.kind)) { Fail(flags.pc, "address operation has invalid resource kind"); } if (memory.kind == ResourceKind::Scratch) { handle = inst.Arg(0).Resolve().TryInstruction(); if (handle == nullptr || handle->GetOpcode() != ValueOpcode::GetScratchResource || handle->NumArgs() != 0) { Fail(flags.pc, "scratch operation requires GetScratchResource"); } if (m_program.scratch_dwords == 0) { Fail(flags.pc, "scratch operation requires a nonzero AGC per-thread size"); } return; } ValidateAddressHandle(inst.Arg(0), flags.pc); m_info.uses_dma = true; return; } if (memory.kind != ResourceKind::Image || image_info.resource_class == ImageResourceClass::None) { Fail(flags.pc, "image operation has invalid resource kind"); } handle = inst.Arg(0).Resolve().TryInstruction(); const auto* indirect = handle != nullptr ? FindIndirectImage(*handle) : nullptr; if (indirect != nullptr) { source = indirect->source; } else { GetHandle(inst.Arg(0), ValueOpcode::GetImageResource, 8, flags.pc, handle, source); } resource = AddImage(source, memory, op, flags.pc); if (resource == UINT32_MAX) { Fail(flags.pc, "image resource limit exceeded"); } AddHandlePatch(handle, resource, flags.pc); uint32_t sampler = 0; if (image_info.needs_sampler) { if (inst.NumArgs() < 2) { Fail(flags.pc, "sampled image operation has no sampler handle"); } Inst* sampler_handle = nullptr; uint32_t sampler_source = 0; const bool sample_adjust = (memory.image_sample_flags & Decoder::ImageSampleFlagAdjust) != 0; GetHandle(inst.Arg(1), ValueOpcode::GetSamplerResource, 4, flags.pc, sampler_handle, sampler_source, true, sample_adjust); sampler = AddSampler(sampler_source, flags.pc); if (sampler == UINT32_MAX) { Fail(flags.pc, "sampler resource limit exceeded"); } AddHandlePatch(sampler_handle, sampler, flags.pc); AddSampledPair(resource, sampler, flags.pc); } AddMemoryPatch(flags.index, resource, sampler, image_info.needs_sampler, flags.pc); } const DescriptorSource* Source(uint32_t source) const { return source < m_sources.size() ? &m_sources[source] : nullptr; } void LinkImageAliases() { for (auto& buffer: m_info.buffers) { const auto* buffer_source = Source(buffer.source); if (buffer_source == nullptr || buffer_source->dword_count != 4) { continue; } for (uint32_t image = 0; image < m_info.images.size(); image++) { const auto* image_source = Source(m_info.images[image].source); if (image_source == nullptr || image_source->dword_count != 8 || image_source->indirect_image.has_value()) { continue; } bool alias = true; for (uint32_t dword = 0; dword < 4; dword++) { alias = alias && EquivalentValue(m_program, buffer_source->dwords[dword], image_source->dwords[dword]); } if (alias) { buffer.image_alias = image; break; } } } } Program& m_program; ShaderInfo m_info; std::vector m_sources; std::vector m_handle_patches; std::vector m_memory_patches; std::vector m_indirect_images; std::vector> m_descriptor_selections; bool m_shader_writes = false; }; } // namespace void TrackResources(Program& program) { Tracker(program).Run(); } } // namespace Libs::Graphics::ShaderRecompiler::IR