Files
KytyPS5/src/graphics/shader/recompiler/ir/ResourceTracking.cpp
T
2026-07-31 11:36:12 +02:00

463 lines
16 KiB
C++

#include "graphics/shader/recompiler/ir/ResourceTracking.h"
#include "graphics/shader/recompiler/ir/ScalarProvenance.h"
#include <algorithm>
#include <fmt/format.h>
#include <functional>
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<uint64_t>(inst.memory.offset) + static_cast<uint64_t>(bytes) * count;
return end > UINT32_MAX ? UINT32_MAX : static_cast<uint32_t>(end);
}
bool ContainsUnknown(const ScalarProvenance& provenance, uint32_t id, std::vector<uint8_t>& visited,
std::vector<uint32_t>& 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<uint8_t>& 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<uint8_t> 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<Instruction> 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<uint8_t> visited(m_program.provenance.values.size());
for (uint32_t i = 0; i < descriptor->dword_count; i++) {
std::vector<uint32_t> 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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(m_info.images.size() - 1);
}
uint32_t AddAddress(const Instruction& inst) {
auto immediate = static_cast<int32_t>(inst.memory.offset);
if (inst.memory.kind == ResourceKind::ScalarBuffer) {
immediate = static_cast<int32_t>(static_cast<uint32_t>(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<uint32_t>(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<uint32_t>(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<Patch> m_patches;
};
} // namespace
bool TrackResources(Program& program, std::string* error) {
return Tracker(program).Run(error);
}
} // namespace Libs::Graphics::ShaderRecompiler::IR