#include "graphics/shader/recompiler/ir/ResourceTracking.h" #include "graphics/shader/recompiler/ir/ScalarProvenance.h" #include #include #include namespace Libs::Graphics::ShaderRecompiler::IR { namespace { 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"; } } bool IsAtomic(Opcode op) { switch (op) { case Opcode::AtomicSwapU32: case Opcode::AtomicAddU32: case Opcode::AtomicSubU32: case Opcode::AtomicSMinI32: case Opcode::AtomicUMinU32: case Opcode::AtomicSMaxI32: case Opcode::AtomicUMaxU32: case Opcode::AtomicAndU32: case Opcode::AtomicOrU32: case Opcode::AtomicXorU32: return true; default: return false; } } bool IsWrite(Opcode op) { switch (op) { case Opcode::BufferStoreByte: case Opcode::BufferStoreShort: case Opcode::BufferStoreDword: case Opcode::ImageStore: return true; default: return IsAtomic(op); } } bool NeedsSampler(Opcode op) { return op == Opcode::ImageSample || op == Opcode::ImageGather4 || op == Opcode::ImageGetLod; } bool IsDepthCompare(const Instruction& inst) { return (inst.memory.image_sample_flags & Decoder::ImageSampleFlagCompare) != 0; } ImageMipMode MipMode(const Instruction& inst) { const bool storage = inst.memory.kind == ResourceKind::StorageImage || inst.memory.kind == ResourceKind::StorageImageUint; return storage && inst.memory.image_has_mip ? ImageMipMode::DynamicStorage : ImageMipMode::None; } bool IsBuffer(const Instruction& inst) { return inst.memory.kind == ResourceKind::Buffer || (inst.memory.kind == ResourceKind::ScalarBuffer && inst.op == Opcode::SBufferLoadDword); } bool IsImage(const Instruction& inst) { return inst.memory.kind == ResourceKind::Image || inst.memory.kind == ResourceKind::ImageUint || inst.memory.kind == ResourceKind::StorageImage || inst.memory.kind == ResourceKind::StorageImageUint; } bool IsAddress(const Instruction& inst) { return inst.op == Opcode::SLoadDword || inst.memory.kind == ResourceKind::Flat || inst.memory.kind == ResourceKind::Global || inst.memory.kind == ResourceKind::Scratch; } uint32_t ByteExtent(const Instruction& inst) { const auto bytes = std::max((inst.memory.data_bits + 7u) / 8u, 1u); const auto count = std::max(inst.memory.data_dwords, 1u); const auto end = static_cast(inst.memory.offset) + static_cast(bytes) * count; return end > UINT32_MAX ? UINT32_MAX : static_cast(end); } bool ContainsUnknown(const ScalarProvenance& provenance, uint32_t id, std::vector& visited, std::vector& path) { path.push_back(id); if (id <= ScalarProvenance::Unknown || id >= provenance.values.size()) { return true; } if (visited[id] != 0) { path.pop_back(); return false; } visited[id] = 1; const auto& value = provenance.values[id]; if (value.op == ScalarValueOp::Phi) { for (const auto arg: value.phi_args) { if (ContainsUnknown(provenance, arg, visited, path)) { return true; } } path.pop_back(); return false; } const auto args = ScalarValueArgCount(value.op); for (uint32_t i = 0; i < args; i++) { if (ContainsUnknown(provenance, value.args[i], visited, path)) { return true; } } path.pop_back(); return false; } bool IsLoopInvariantValue(const ScalarProvenance& provenance, uint32_t id, std::vector& visiting) { if (id <= ScalarProvenance::Unknown || id >= provenance.values.size()) { return false; } if (visiting[id] != 0) { return true; } visiting[id] = 1; const auto& value = provenance.values[id]; if (value.op == ScalarValueOp::Phi) { uint32_t invariant = ScalarProvenance::Undefined; for (const auto arg: value.phi_args) { if (arg == id) { continue; } if (invariant == ScalarProvenance::Undefined) { invariant = arg; } else if (arg != invariant) { return false; } } return invariant != ScalarProvenance::Undefined && IsLoopInvariantValue(provenance, invariant, visiting); } for (uint32_t i = 0; i < ScalarValueArgCount(value.op); i++) { if (!IsLoopInvariantValue(provenance, value.args[i], visiting)) { return false; } } return true; } bool IsLoopInvariantDescriptor(const ScalarProvenance& provenance, const DescriptorValue& descriptor) { std::vector visiting(provenance.values.size()); for (uint32_t i = 0; i < descriptor.dword_count; i++) { if (!IsLoopInvariantValue(provenance, descriptor.dwords[i], visiting)) { return false; } } return true; } class Tracker { public: explicit Tracker(Program& program): m_program(program) {} bool Run(std::string* error) { if (m_program.resource_tracking_complete) { return Fail(0, error, "resources already tracked"); } if (!m_program.srt_plan_complete) { return Fail(0, error, "SRT plan is not ready"); } if (!m_program.srt_patching_complete) { return Fail(0, error, "SRT reads were not patched"); } for (auto& block: m_program.blocks) { for (auto& inst: block.instructions) { if (!Collect(inst, error)) { return false; } } } LinkImageAliases(); m_program.info = std::move(m_info); for (const auto& patch: m_patches) { auto& inst = patch.inst.get(); inst.memory.resource = patch.resource; inst.memory.sampler = patch.sampler; inst.memory.resource_source = ScalarProvenance::Undefined; inst.memory.sampler_source = ScalarProvenance::Undefined; } m_program.resource_tracking_complete = true; return true; } private: struct Patch { std::reference_wrapper inst; uint32_t resource = 0; uint32_t sampler = 0; }; bool Fail(uint32_t pc, std::string* error, const std::string& reason) const { if (error != nullptr) { *error = fmt::format("shader resource tracking: hash=0x{:016x} stage={} pc=0x{:08x} {}", m_program.shader_hash, StageName(m_program.stage), pc, reason); } return false; } bool ValidateSource(uint32_t source, uint32_t dwords, uint32_t pc, std::string* error) const { const auto* descriptor = GetDescriptorSource(m_program, source); if (descriptor == nullptr || descriptor->dword_count != dwords) { return Fail(pc, error, fmt::format("descriptor source {} is missing or has wrong width", source)); } std::vector visited(m_program.provenance.values.size()); for (uint32_t i = 0; i < descriptor->dword_count; i++) { std::vector path; if (ContainsUnknown(m_program.provenance, descriptor->dwords[i], visited, path)) { const auto value = descriptor->dwords[i]; std::string chain; for (const auto id: path) { const auto op = id < m_program.provenance.values.size() ? static_cast(m_program.provenance.values[id].op) : UINT32_MAX; chain += fmt::format("{}{}:{}({})", chain.empty() ? "" : " -> ", id, op, ScalarValueToString(m_program.provenance, id)); } return Fail( pc, error, fmt::format( "descriptor source {} dword {} contains an unknown value {} ({}) path {}", source, i, value, ScalarValueToString(m_program.provenance, value), chain)); } } const auto dynamic = std::find(m_program.srt.dynamic_sources.begin(), m_program.srt.dynamic_sources.end(), source) != m_program.srt.dynamic_sources.end(); if (dynamic && !IsLoopInvariantDescriptor(m_program.provenance, *descriptor)) { std::string detail; for (uint32_t i = 0; i < descriptor->dword_count; i++) { const auto id = descriptor->dwords[i]; detail += fmt::format(" d{}={}({})", i, id, id < m_program.provenance.values.size() ? static_cast(m_program.provenance.values[id].op) : UINT32_MAX); if (id < m_program.provenance.values.size()) { for (const auto arg: m_program.provenance.values[id].phi_args) { detail += fmt::format( "/{}({})", arg, arg < m_program.provenance.values.size() ? static_cast(m_program.provenance.values[arg].op) : UINT32_MAX); } } } return Fail(pc, error, fmt::format("descriptor source {} requires unsupported GPU selection{}", source, detail)); } if (!DescriptorSourceResolved(m_program, source) && !dynamic) { return Fail(pc, error, fmt::format("descriptor source {} is unresolved", source)); } return true; } uint32_t AddBuffer(const Instruction& inst) { for (uint32_t i = 0; i < m_info.buffers.size(); i++) { if (m_info.buffers[i].source == inst.memory.resource_source) { Merge(&m_info.buffers[i], inst); return i; } } if (m_info.buffers.size() >= ShaderInfo::MaxBuffers) { return UINT32_MAX; } BufferResource resource; resource.source = inst.memory.resource_source; resource.first_use_pc = inst.pc; Merge(&resource, inst); m_info.buffers.push_back(resource); return static_cast(m_info.buffers.size() - 1); } void Merge(BufferResource* resource, const Instruction& inst) const { resource->first_use_pc = std::min(resource->first_use_pc, inst.pc); resource->max_byte_extent = std::max(resource->max_byte_extent, ByteExtent(inst)); resource->read = resource->read || !IsWrite(inst.op) || IsAtomic(inst.op); resource->written = resource->written || IsWrite(inst.op); resource->atomic = resource->atomic || IsAtomic(inst.op); resource->formatted = resource->formatted || inst.memory.formatted; resource->scalar = resource->scalar || inst.memory.kind == ResourceKind::ScalarBuffer; } void LinkImageAliases() { for (auto& buffer: m_info.buffers) { const auto* buffer_source = GetDescriptorSource(m_program, buffer.source); if (buffer_source == nullptr || buffer_source->dword_count != 4) { continue; } for (uint32_t i = 0; i < m_info.images.size(); i++) { const auto* image_source = GetDescriptorSource(m_program, m_info.images[i].source); if (image_source != nullptr && image_source->dword_count == 8 && std::equal(buffer_source->dwords.begin(), buffer_source->dwords.begin() + 4, image_source->dwords.begin())) { buffer.image_alias = i; break; } } } } uint32_t AddImage(const Instruction& inst) { for (uint32_t i = 0; i < m_info.images.size(); i++) { auto& image = m_info.images[i]; if (image.source == inst.memory.resource_source && image.kind == inst.memory.kind && image.dimension == inst.memory.image_dimension && image.mip_mode == MipMode(inst) && image.depth_compare == IsDepthCompare(inst)) { Merge(&image, inst); return i; } } if (m_info.images.size() >= ShaderInfo::MaxImages) { return UINT32_MAX; } ImageResource image; image.source = inst.memory.resource_source; image.first_use_pc = inst.pc; image.kind = inst.memory.kind; image.dimension = inst.memory.image_dimension; image.mip_mode = MipMode(inst); image.depth_compare = IsDepthCompare(inst); Merge(&image, inst); m_info.images.push_back(image); return static_cast(m_info.images.size() - 1); } uint32_t AddAddress(const Instruction& inst) { auto immediate = static_cast(inst.memory.offset); if (inst.memory.kind == ResourceKind::ScalarBuffer) { immediate = static_cast(static_cast(immediate) & ~3u); } const auto min_offset = inst.memory.resource_source == ScalarProvenance::Unknown ? 0 : std::min(immediate, 0); for (uint32_t i = 0; i < m_info.addresses.size(); i++) { auto& address = m_info.addresses[i]; if (address.source == inst.memory.resource_source && address.kind == inst.memory.kind) { address.first_use_pc = std::min(address.first_use_pc, inst.pc); address.min_offset = std::min(address.min_offset, min_offset); address.read = address.read || !IsWrite(inst.op) || IsAtomic(inst.op); address.written = address.written || IsWrite(inst.op); address.atomic = address.atomic || IsAtomic(inst.op); return i; } } if (m_info.addresses.size() >= ShaderInfo::MaxAddresses) { return UINT32_MAX; } AddressResource address {inst.memory.resource_source, inst.pc, inst.memory.kind, min_offset}; address.read = !IsWrite(inst.op) || IsAtomic(inst.op); address.written = IsWrite(inst.op); address.atomic = IsAtomic(inst.op); m_info.addresses.push_back(address); return static_cast(m_info.addresses.size() - 1); } void Merge(ImageResource* resource, const Instruction& inst) const { resource->first_use_pc = std::min(resource->first_use_pc, inst.pc); resource->read = resource->read || !IsWrite(inst.op) || IsAtomic(inst.op); resource->written = resource->written || IsWrite(inst.op); resource->atomic = resource->atomic || IsAtomic(inst.op); } uint32_t AddSampler(const Instruction& inst) { for (uint32_t i = 0; i < m_info.samplers.size(); i++) { if (m_info.samplers[i].source == inst.memory.sampler_source) { m_info.samplers[i].first_use_pc = std::min(m_info.samplers[i].first_use_pc, inst.pc); return i; } } if (m_info.samplers.size() >= ShaderInfo::MaxSamplers) { return UINT32_MAX; } m_info.samplers.push_back({inst.memory.sampler_source, inst.pc}); return static_cast(m_info.samplers.size() - 1); } bool AddSampledPair(uint32_t image, uint32_t sampler, uint32_t pc, std::string* error) { 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 true; } } if (m_info.sampled_pairs.size() >= ShaderInfo::MaxSampledPairs) { return Fail(pc, error, "sampled image/sampler pair limit exceeded"); } m_info.sampled_pairs.push_back({image, sampler, pc}); return true; } bool Collect(Instruction& inst, std::string* error) { if (IsAddress(inst)) { const bool unbased = inst.memory.resource_source == ScalarProvenance::Unknown; if ((!unbased && !ValidateSource(inst.memory.resource_source, 2, inst.pc, error)) || (unbased && inst.memory.kind != ResourceKind::Flat && inst.memory.kind != ResourceKind::Global && inst.memory.kind != ResourceKind::Scratch)) { return unbased ? Fail(inst.pc, error, "scalar memory base is unresolved") : false; } const auto resource = AddAddress(inst); if (resource == UINT32_MAX) { return Fail(inst.pc, error, "address resource limit exceeded"); } m_patches.push_back({std::ref(inst), resource, 0}); return true; } if (!IsBuffer(inst) && !IsImage(inst)) { return true; } if (!ValidateSource(inst.memory.resource_source, IsBuffer(inst) ? 4u : 8u, inst.pc, error)) { return false; } const auto resource = IsBuffer(inst) ? AddBuffer(inst) : AddImage(inst); if (resource == UINT32_MAX) { return Fail(inst.pc, error, IsBuffer(inst) ? "buffer resource limit exceeded" : "image resource limit exceeded"); } uint32_t sampler = 0; if (NeedsSampler(inst.op)) { if (!ValidateSource(inst.memory.sampler_source, 4, inst.pc, error)) { return false; } sampler = AddSampler(inst); if (sampler == UINT32_MAX) { return Fail(inst.pc, error, "sampler resource limit exceeded"); } if (!AddSampledPair(resource, sampler, inst.pc, error)) { return false; } } m_patches.push_back({std::ref(inst), resource, sampler}); return true; } Program& m_program; ShaderInfo m_info; std::vector m_patches; }; } // namespace bool TrackResources(Program& program, std::string* error) { return Tracker(program).Run(error); } } // namespace Libs::Graphics::ShaderRecompiler::IR