mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-19 18:36:53 +02:00
744 lines
29 KiB
C++
744 lines
29 KiB
C++
#include "common/assert.h"
|
|
#include "graphics/shader/recompiler/ir/ShaderIR.h"
|
|
#include "graphics/shader/recompiler/BufferFormat.h"
|
|
|
|
#include <fmt/format.h>
|
|
#include <xbyak/xbyak.h>
|
|
#include <map>
|
|
#include <new>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <utility>
|
|
|
|
namespace Libs::Graphics::ShaderRecompiler::IR {
|
|
|
|
namespace {
|
|
|
|
[[noreturn]] void Fail(std::string_view message) {
|
|
EXIT("shader IR validation failed: %s", std::string(message).c_str());
|
|
std::abort();
|
|
}
|
|
|
|
bool IsRegisterStatePseudo(ValueOpcode opcode) {
|
|
switch (opcode) {
|
|
case ValueOpcode::GetThreadBitScalarRegister:
|
|
case ValueOpcode::SetThreadBitScalarRegister:
|
|
case ValueOpcode::GetScalarMaskTag:
|
|
case ValueOpcode::SetScalarMaskTag:
|
|
case ValueOpcode::GetScalarRegister:
|
|
case ValueOpcode::SetScalarRegister:
|
|
case ValueOpcode::GetVectorRegister:
|
|
case ValueOpcode::SetVectorRegister:
|
|
case ValueOpcode::GetGotoVariable:
|
|
case ValueOpcode::SetGotoVariable:
|
|
case ValueOpcode::GetScc:
|
|
case ValueOpcode::SetScc:
|
|
case ValueOpcode::GetExec:
|
|
case ValueOpcode::SetExec:
|
|
case ValueOpcode::GetExecLo:
|
|
case ValueOpcode::SetExecLo:
|
|
case ValueOpcode::GetExecHi:
|
|
case ValueOpcode::SetExecHi:
|
|
case ValueOpcode::GetVcc:
|
|
case ValueOpcode::SetVcc:
|
|
case ValueOpcode::GetVccLo:
|
|
case ValueOpcode::SetVccLo:
|
|
case ValueOpcode::GetVccHi:
|
|
case ValueOpcode::SetVccHi:
|
|
case ValueOpcode::GetM0:
|
|
case ValueOpcode::SetM0: return true;
|
|
default: return false;
|
|
}
|
|
}
|
|
|
|
bool IsRuntimeRead(ValueOpcode opcode) {
|
|
return opcode == ValueOpcode::ReadConstBuffer ||
|
|
AddressOpcodeInfoOf(opcode).access == AddressAccess::Read;
|
|
}
|
|
|
|
bool EquivalentValue(const ResourcePlan& program, Value left, Value right,
|
|
std::vector<std::pair<const Inst*, const Inst*>>& visited) {
|
|
left = left.Resolve();
|
|
right = right.Resolve();
|
|
if (left == right) {
|
|
return true;
|
|
}
|
|
if (left.IsImmediate() || right.IsImmediate() || left.GetType() != right.GetType()) {
|
|
return false;
|
|
}
|
|
const auto* lhs = left.TryInstruction();
|
|
const auto* rhs = right.TryInstruction();
|
|
if (lhs == nullptr || rhs == nullptr || lhs->GetOpcode() != rhs->GetOpcode() ||
|
|
lhs->NumArgs() != rhs->NumArgs()) {
|
|
return false;
|
|
}
|
|
if (std::ranges::find(visited, std::pair {lhs, rhs}) != visited.end()) {
|
|
return true;
|
|
}
|
|
visited.emplace_back(lhs, rhs);
|
|
if (IsRuntimeRead(lhs->GetOpcode())) {
|
|
const auto li = lhs->Flags<MemoryFlags>().index;
|
|
const auto ri = rhs->Flags<MemoryFlags>().index;
|
|
if (li >= program.memory_info.size() || ri >= program.memory_info.size() ||
|
|
program.memory_info[li] != program.memory_info[ri]) {
|
|
return false;
|
|
}
|
|
} else if (lhs->GetOpcode() != ValueOpcode::ReadConst &&
|
|
lhs->Flags<uint64_t>() != rhs->Flags<uint64_t>()) {
|
|
return false;
|
|
}
|
|
for (size_t index = 0; index < lhs->NumArgs(); index++) {
|
|
if (lhs->GetOpcode() == ValueOpcode::Phi && lhs->PhiBlock(index) != rhs->PhiBlock(index)) {
|
|
return false;
|
|
}
|
|
if (!EquivalentValue(program, lhs->Arg(index), rhs->Arg(index), visited)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
ResourcePlan::ResourcePlan() = default;
|
|
ResourcePlan::ResourcePlan(ResourcePlan&&) noexcept = default;
|
|
|
|
ResourcePlan::~ResourcePlan() {
|
|
for (auto& inst: value_storage) {
|
|
inst.Invalidate();
|
|
}
|
|
}
|
|
|
|
ResourcePlan& ResourcePlan::operator=(ResourcePlan&& other) noexcept {
|
|
if (this != &other) {
|
|
this->~ResourcePlan();
|
|
new (this) ResourcePlan(std::move(other));
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
Program::~Program() {
|
|
// Planning expressions can refer to block values but outlive block storage in the base class.
|
|
for (auto& inst: value_storage) {
|
|
inst.Invalidate();
|
|
}
|
|
// Values may cross block boundaries. Detach all arguments before any block starts destroying
|
|
// its instruction storage so reverse-use links always point to live definitions.
|
|
for (auto* block: blocks) {
|
|
for (auto& inst: *block) {
|
|
inst.Invalidate();
|
|
}
|
|
}
|
|
}
|
|
|
|
Program& Program::operator=(Program&& other) noexcept {
|
|
if (this != &other) {
|
|
this->~Program();
|
|
new (this) Program(std::move(other));
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
CompiledShaderInfo Program::TakeCompiledInfo() && {
|
|
CompiledShaderInfo result {
|
|
.stage = stage,
|
|
.shader_hash = shader_hash,
|
|
.wave_size = wave_size,
|
|
.user_data_base = user_data_base,
|
|
.user_data_count = user_data_count,
|
|
.scratch_dwords = scratch_dwords,
|
|
.has_address_writes = has_address_writes,
|
|
.info = std::move(info),
|
|
.bindings = std::move(bindings),
|
|
};
|
|
for (const auto& output: result.info.outputs) {
|
|
if (output.kind == StageOutputKind::Parameter && output.index < 32) {
|
|
result.param_export_mask |= 1u << output.index;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool EquivalentValue(const ResourcePlan& program, Value left, Value right) {
|
|
std::vector<std::pair<const Inst*, const Inst*>> visited;
|
|
return EquivalentValue(program, left, right, visited);
|
|
}
|
|
|
|
bool EquivalentSelectorDomain(const ResourcePlan& program,
|
|
const DescriptorSource::IndirectDescriptor& left,
|
|
const DescriptorSource::IndirectDescriptor& right) {
|
|
return left.selector == right.selector && left.signed_count == right.signed_count &&
|
|
left.workgroup_axis == right.workgroup_axis &&
|
|
EquivalentValue(program, left.key_count, right.key_count) &&
|
|
EquivalentValue(program, left.selector_first, right.selector_first) &&
|
|
EquivalentValue(program, left.selector_mask, right.selector_mask);
|
|
}
|
|
|
|
Value ResolveInvariantPhi(const ResourcePlan& program, Value value) {
|
|
value = value.Resolve();
|
|
const auto* root = value.TryInstruction();
|
|
if (root == nullptr || root->GetOpcode() != ValueOpcode::Phi) {
|
|
return value;
|
|
}
|
|
Value invariant;
|
|
std::vector<Value> pending {value};
|
|
std::unordered_set<const Inst*> visited;
|
|
while (!pending.empty()) {
|
|
const auto current = pending.back().Resolve();
|
|
pending.pop_back();
|
|
const auto* inst = current.TryInstruction();
|
|
if (inst != nullptr && inst->GetOpcode() == ValueOpcode::Phi) {
|
|
if (!visited.insert(inst).second) {
|
|
continue;
|
|
}
|
|
for (size_t index = 0; index < inst->NumArgs(); index++) {
|
|
pending.push_back(inst->Arg(index));
|
|
}
|
|
continue;
|
|
}
|
|
if (invariant.IsEmpty()) {
|
|
invariant = current;
|
|
} else if (!EquivalentValue(program, invariant, current)) {
|
|
return {};
|
|
}
|
|
}
|
|
return invariant;
|
|
}
|
|
|
|
Value ResolveActiveU32(Value value, Value active) {
|
|
for (uint32_t depth = 0; depth <= 32; ++depth) {
|
|
value = value.Resolve();
|
|
const auto* inst = value.TryInstruction();
|
|
if (inst == nullptr || inst->GetOpcode() != ValueOpcode::SelectU32 ||
|
|
inst->Arg(0).Resolve() != active)
|
|
return value;
|
|
value = inst->Arg(1);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
uint32_t StorageBufferElementBits(const Program& program, const MemoryInfo& memory) {
|
|
if (!memory.formatted) return memory.data_bits;
|
|
const auto format = memory.typed
|
|
? Format::DecodeTBufferFormat(memory.data_format, memory.number_format)
|
|
: program.info.buffers[memory.resource].descriptor_format;
|
|
const auto info = Format::GetFormatInfo(format);
|
|
return info.type == Format::ComponentType::Unknown || info.packed_bitfield
|
|
? 32u : info.component_bits[0];
|
|
}
|
|
|
|
bool HasShaderMemoryWrites(const Program& program) {
|
|
for (const auto* block: program.blocks) {
|
|
for (const auto& inst: *block) {
|
|
const auto op = inst.GetOpcode();
|
|
const auto buffer = BufferAccessOf(op);
|
|
const auto image = ImageOpcodeInfoOf(op).access;
|
|
if (buffer == BufferAccess::Write || buffer == BufferAccess::Atomic ||
|
|
image == ImageAccess::Write || image == ImageAccess::Atomic ||
|
|
AddressOpcodeInfoOf(op).access == AddressAccess::Write) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void ValidateProgram(const Program& program, bool require_ssa) {
|
|
if (program.blocks.size() != program.block_storage.size()) {
|
|
return Fail("value IR block storage is inconsistent");
|
|
}
|
|
std::unordered_map<const Block*, size_t> block_indices;
|
|
std::unordered_set<uint32_t> block_ids;
|
|
std::unordered_map<const Inst*, size_t> instruction_positions;
|
|
for (size_t block_index = 0; block_index < program.blocks.size(); block_index++) {
|
|
const auto* block = program.blocks[block_index];
|
|
if (block == nullptr || program.block_storage[block_index] == nullptr ||
|
|
block != program.block_storage[block_index].get()) {
|
|
return Fail("value IR block pointer is inconsistent");
|
|
}
|
|
if (!block_indices.emplace(block, block_index).second) {
|
|
return Fail("value IR block pointer is duplicated");
|
|
}
|
|
if (block->id == UINT32_MAX) {
|
|
return Fail("value IR block uses the reserved exit id");
|
|
}
|
|
if (!block_ids.insert(block->id).second) {
|
|
return Fail("value IR block id is duplicated");
|
|
}
|
|
size_t position = 0;
|
|
for (const auto& inst: *block) {
|
|
if (!instruction_positions.emplace(&inst, position++).second) {
|
|
return Fail("value IR instruction is duplicated");
|
|
}
|
|
}
|
|
}
|
|
if (!program.blocks.empty() && !program.blocks.front()->ImmPredecessors().empty()) {
|
|
return Fail("value IR entry block has a predecessor");
|
|
}
|
|
|
|
for (size_t block_index = 0; block_index < program.blocks.size(); block_index++) {
|
|
const auto* block = program.blocks[block_index];
|
|
std::unordered_set<const Block*> predecessors;
|
|
for (const auto* predecessor: block->ImmPredecessors()) {
|
|
if (predecessor == nullptr || !block_indices.contains(predecessor)) {
|
|
return Fail("value IR block has a foreign predecessor");
|
|
}
|
|
if (!predecessors.insert(predecessor).second) {
|
|
return Fail("value IR block predecessor is duplicated");
|
|
}
|
|
if (std::ranges::find(predecessor->ImmSuccessors(), block) ==
|
|
predecessor->ImmSuccessors().end()) {
|
|
return Fail("value IR predecessor edge is not reciprocal");
|
|
}
|
|
}
|
|
|
|
std::unordered_set<const Block*> successors;
|
|
for (const auto* successor: block->ImmSuccessors()) {
|
|
if (successor == nullptr || !block_indices.contains(successor)) {
|
|
return Fail("value IR block has a foreign successor");
|
|
}
|
|
if (!successors.insert(successor).second) {
|
|
return Fail("value IR block successor is duplicated");
|
|
}
|
|
if (std::ranges::find(successor->ImmPredecessors(), block) ==
|
|
successor->ImmPredecessors().end()) {
|
|
return Fail("value IR successor edge is not reciprocal");
|
|
}
|
|
}
|
|
|
|
std::unordered_set<const Block*> expected_successors;
|
|
const auto add_target = [&](const Block* target) {
|
|
if (!block_indices.contains(target)) {
|
|
return false;
|
|
}
|
|
expected_successors.insert(target);
|
|
return true;
|
|
};
|
|
const auto& terminator = block->terminator;
|
|
const auto validate_control_value = [&](Value value, Type type) {
|
|
if (value.IsEmpty() || value.GetType() != type) {
|
|
return false;
|
|
}
|
|
const auto* definition = value.TryInstruction();
|
|
return definition == nullptr || instruction_positions.contains(definition);
|
|
};
|
|
switch (terminator.kind) {
|
|
case CFG::TerminatorKind::Branch:
|
|
if (!add_target(terminator.true_block)) {
|
|
return Fail("value IR branch target is missing");
|
|
}
|
|
break;
|
|
case CFG::TerminatorKind::ConditionalBranch:
|
|
if (!add_target(terminator.true_block) || !add_target(terminator.false_block)) {
|
|
return Fail("value IR conditional branch target is missing");
|
|
}
|
|
if (!validate_control_value(block->condition, Type::U1)) {
|
|
return Fail("value IR conditional branch condition is invalid");
|
|
}
|
|
break;
|
|
case CFG::TerminatorKind::IndirectBranch: {
|
|
if (!validate_control_value(block->indirect_target, Type::U32)) {
|
|
return Fail("value IR indirect branch selector is invalid");
|
|
}
|
|
std::unordered_set<uint32_t> values;
|
|
for (const auto& branch: terminator.cases) {
|
|
if (!values.insert(branch.value).second) {
|
|
return Fail("value IR indirect selector value is duplicated");
|
|
}
|
|
if (!add_target(branch.target)) {
|
|
return Fail("value IR indirect branch target is missing");
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case CFG::TerminatorKind::Return:
|
|
case CFG::TerminatorKind::Unsupported: break;
|
|
}
|
|
if ((terminator.merge_block != nullptr &&
|
|
!block_indices.contains(terminator.merge_block)) ||
|
|
(terminator.continue_block != nullptr &&
|
|
!block_indices.contains(terminator.continue_block))) {
|
|
return Fail("value IR structured control target is missing");
|
|
}
|
|
if (successors != expected_successors) {
|
|
return Fail("value IR terminator and successor graph disagree");
|
|
}
|
|
|
|
bool saw_non_phi = false;
|
|
for (const auto& inst: *block) {
|
|
if (inst.Parent() != block) {
|
|
return Fail("value IR instruction has the wrong parent block");
|
|
}
|
|
if (inst.GetOpcode() == ValueOpcode::Phi) {
|
|
if (saw_non_phi) {
|
|
return Fail("value IR Phi appears after a non-Phi instruction");
|
|
}
|
|
if (inst.NumPhiBlocks() != inst.NumArgs()) {
|
|
return Fail("value IR Phi parent table is inconsistent");
|
|
}
|
|
if (inst.NumArgs() == 0) {
|
|
return Fail("value IR Phi has no incoming values");
|
|
}
|
|
std::unordered_set<const Block*> incoming_blocks;
|
|
for (size_t arg_index = 0; arg_index < inst.NumArgs(); arg_index++) {
|
|
const auto* predecessor = inst.PhiBlock(arg_index);
|
|
if (predecessor == nullptr || !block_indices.contains(predecessor) ||
|
|
!predecessors.contains(predecessor)) {
|
|
return Fail("value IR Phi has a foreign or non-predecessor parent");
|
|
}
|
|
if (!incoming_blocks.insert(predecessor).second) {
|
|
return Fail("value IR Phi parent is duplicated");
|
|
}
|
|
if (inst.Arg(arg_index).GetType() != inst.GetType()) {
|
|
return Fail("value IR Phi incoming type does not match its result");
|
|
}
|
|
}
|
|
if (incoming_blocks != predecessors) {
|
|
return Fail("value IR Phi does not cover every predecessor");
|
|
}
|
|
} else {
|
|
saw_non_phi = true;
|
|
}
|
|
if (require_ssa && IsRegisterStatePseudo(inst.GetOpcode())) {
|
|
return Fail(fmt::format("register-state pseudo {} survived SSA rewrite",
|
|
ValueOpcodeName(inst.GetOpcode())));
|
|
}
|
|
if (inst.GetOpcode() != ValueOpcode::Phi && inst.GetOpcode() != ValueOpcode::Identity &&
|
|
inst.GetType() == Type::Opaque) {
|
|
return Fail(fmt::format("untyped opcode {} survived translation",
|
|
ValueOpcodeName(inst.GetOpcode())));
|
|
}
|
|
const bool fixed_signature =
|
|
inst.GetOpcode() != ValueOpcode::Phi && inst.GetOpcode() != ValueOpcode::Identity;
|
|
if (fixed_signature && inst.NumArgs() != NumArgsOf(inst.GetOpcode())) {
|
|
return Fail(fmt::format("{} has {} arguments, expected {}",
|
|
ValueOpcodeName(inst.GetOpcode()), inst.NumArgs(),
|
|
NumArgsOf(inst.GetOpcode())));
|
|
}
|
|
if (inst.GetOpcode() == ValueOpcode::ReadConstBuffer) {
|
|
const auto memory_index = inst.Flags<MemoryFlags>().index;
|
|
if (memory_index >= program.memory_info.size()) {
|
|
return Fail(fmt::format("{} has an invalid memory-info index",
|
|
ValueOpcodeName(inst.GetOpcode())));
|
|
}
|
|
const auto& memory = program.memory_info[memory_index];
|
|
if (memory.kind != ResourceKind::ScalarBuffer &&
|
|
memory.kind != ResourceKind::IndirectBuffer) {
|
|
return Fail(fmt::format("{} has an invalid scalar-memory resource kind",
|
|
ValueOpcodeName(inst.GetOpcode())));
|
|
}
|
|
const bool valid_group_width =
|
|
memory.component_count == 1u || memory.component_count == 2u ||
|
|
memory.component_count == 4u || memory.component_count == 8u ||
|
|
memory.component_count == 16u;
|
|
if (memory.data_bits != 32u || memory.data_dwords != 1u || !valid_group_width ||
|
|
memory.component_index >= memory.component_count) {
|
|
return Fail(fmt::format("{} has inconsistent scalar-memory metadata",
|
|
ValueOpcodeName(inst.GetOpcode())));
|
|
}
|
|
}
|
|
const auto address_info = AddressOpcodeInfoOf(inst.GetOpcode());
|
|
if (address_info.access != AddressAccess::None) {
|
|
const auto memory_index = inst.Flags<MemoryFlags>().index;
|
|
if (memory_index >= program.memory_info.size()) {
|
|
return Fail(fmt::format("{} has an invalid memory-info index",
|
|
ValueOpcodeName(inst.GetOpcode())));
|
|
}
|
|
const auto& memory = program.memory_info[memory_index];
|
|
if (!IsAddressResourceKind(memory.kind) ||
|
|
(memory.kind == ResourceKind::ScalarAddress &&
|
|
inst.GetOpcode() != ValueOpcode::LoadAddressU32)) {
|
|
return Fail(fmt::format("{} has an invalid address resource kind",
|
|
ValueOpcodeName(inst.GetOpcode())));
|
|
}
|
|
const bool scalar_address = memory.kind == ResourceKind::ScalarAddress;
|
|
const bool valid_group_width =
|
|
scalar_address
|
|
? memory.component_count == 1u || memory.component_count == 2u ||
|
|
memory.component_count == 4u || memory.component_count == 8u ||
|
|
memory.component_count == 16u
|
|
: memory.component_count >= 1u && memory.component_count <= 4u;
|
|
if (memory.data_bits != address_info.data_bits || memory.data_dwords != 1u ||
|
|
!valid_group_width || memory.component_index >= memory.component_count ||
|
|
memory.sampler != 0u) {
|
|
return Fail(fmt::format("{} has inconsistent address-memory metadata",
|
|
ValueOpcodeName(inst.GetOpcode())));
|
|
}
|
|
}
|
|
const auto buffer_components = BufferComponentCount(inst.GetOpcode());
|
|
if (buffer_components != 0u) {
|
|
const auto memory_index = inst.Flags<MemoryFlags>().index;
|
|
if (memory_index >= program.memory_info.size()) {
|
|
return Fail(fmt::format("{} has an invalid memory-info index",
|
|
ValueOpcodeName(inst.GetOpcode())));
|
|
}
|
|
const auto& memory = program.memory_info[memory_index];
|
|
const bool vector_buffer = memory.kind == ResourceKind::Buffer ||
|
|
memory.kind == ResourceKind::IndirectBuffer;
|
|
if (!vector_buffer && memory.kind != ResourceKind::ScalarBuffer) {
|
|
return Fail(fmt::format("{} has a non-buffer resource kind",
|
|
ValueOpcodeName(inst.GetOpcode())));
|
|
}
|
|
if (memory.kind == ResourceKind::IndirectBuffer &&
|
|
!memory.SupportsIndirectBufferLoad(inst.GetOpcode())) {
|
|
return Fail("indirect buffer requires a scalar, raw 8/16-bit, raw DWORD x1/x2/x3/x4, "
|
|
"or formatted X load");
|
|
}
|
|
const auto access = BufferAccessOf(inst.GetOpcode());
|
|
const bool packed_formatted =
|
|
(access == BufferAccess::Read || access == BufferAccess::Write) && memory.formatted &&
|
|
memory.data_bits == 16u && memory.component_count == 4u && buffer_components == 2u;
|
|
if (buffer_components > 1u &&
|
|
(!vector_buffer || (memory.data_bits != 32u && !packed_formatted) ||
|
|
memory.data_dwords != buffer_components || memory.component_index != 0u)) {
|
|
return Fail(fmt::format("{} has inconsistent native-wide metadata",
|
|
ValueOpcodeName(inst.GetOpcode())));
|
|
}
|
|
if (buffer_components == 1u &&
|
|
(inst.GetOpcode() == ValueOpcode::LoadBufferU32 ||
|
|
inst.GetOpcode() == ValueOpcode::StoreBufferU32) &&
|
|
memory.data_dwords != 1u) {
|
|
return Fail(fmt::format("{} retains scalar-sibling width metadata",
|
|
ValueOpcodeName(inst.GetOpcode())));
|
|
}
|
|
}
|
|
const auto shared_components = SharedComponentCount(inst.GetOpcode());
|
|
if (shared_components != 0u) {
|
|
const auto memory_index = inst.Flags<MemoryFlags>().index;
|
|
if (memory_index >= program.memory_info.size()) {
|
|
return Fail(fmt::format("{} has an invalid memory-info index",
|
|
ValueOpcodeName(inst.GetOpcode())));
|
|
}
|
|
const auto& memory = program.memory_info[memory_index];
|
|
if ((memory.kind != ResourceKind::Lds && memory.kind != ResourceKind::Gds) ||
|
|
memory.resource != 0u || memory.sampler != 0u || memory.component_count == 0u ||
|
|
memory.component_index >= memory.component_count) {
|
|
return Fail(fmt::format("{} has invalid shared-memory metadata",
|
|
ValueOpcodeName(inst.GetOpcode())));
|
|
}
|
|
uint32_t expected_bits = 32u;
|
|
if (inst.GetOpcode() == ValueOpcode::LoadSharedU8 ||
|
|
inst.GetOpcode() == ValueOpcode::WriteSharedU8) {
|
|
expected_bits = 8u;
|
|
} else if (inst.GetOpcode() == ValueOpcode::LoadSharedU16 ||
|
|
inst.GetOpcode() == ValueOpcode::WriteSharedU16) {
|
|
expected_bits = 16u;
|
|
}
|
|
if (memory.data_bits != expected_bits || memory.data_dwords != shared_components ||
|
|
(shared_components > 1u && memory.component_index != 0u)) {
|
|
return Fail(fmt::format("{} has inconsistent shared-memory width",
|
|
ValueOpcodeName(inst.GetOpcode())));
|
|
}
|
|
}
|
|
const auto image_info = ImageOpcodeInfoOf(inst.GetOpcode());
|
|
if (image_info.access != ImageAccess::None) {
|
|
const auto memory_index = inst.Flags<MemoryFlags>().index;
|
|
if (memory_index >= program.memory_info.size()) {
|
|
return Fail(fmt::format("{} has an invalid memory-info index",
|
|
ValueOpcodeName(inst.GetOpcode())));
|
|
}
|
|
const auto& memory = program.memory_info[memory_index];
|
|
if (memory.kind != ResourceKind::Image ||
|
|
image_info.resource_class == ImageResourceClass::None) {
|
|
return Fail(fmt::format("{} has invalid image-memory metadata",
|
|
ValueOpcodeName(inst.GetOpcode())));
|
|
}
|
|
}
|
|
if (inst.GetOpcode() == ValueOpcode::SetAttribute &&
|
|
inst.Flags<ExportFlags>().index >= program.export_info.size()) {
|
|
return Fail("SetAttribute has an invalid export-info index");
|
|
}
|
|
uint32_t composite_components = 0u;
|
|
switch (inst.GetOpcode()) {
|
|
case ValueOpcode::CompositeExtractU64: composite_components = 2u; break;
|
|
case ValueOpcode::CompositeExtractU32x2: composite_components = 2u; break;
|
|
case ValueOpcode::CompositeExtractU32x3: composite_components = 3u; break;
|
|
case ValueOpcode::CompositeExtractU32x4: composite_components = 4u; break;
|
|
default: break;
|
|
}
|
|
if (composite_components != 0u &&
|
|
(!inst.Arg(1).IsImmediate() || inst.Arg(1).GetType() != Type::U32 ||
|
|
inst.Arg(1).U32() >= composite_components)) {
|
|
return Fail(fmt::format("{} has an invalid component index",
|
|
ValueOpcodeName(inst.GetOpcode())));
|
|
}
|
|
for (size_t arg_index = 0; arg_index < inst.NumArgs(); arg_index++) {
|
|
const auto arg = inst.Arg(arg_index);
|
|
if (arg.IsEmpty()) {
|
|
return Fail(
|
|
fmt::format("{} has an empty argument", ValueOpcodeName(inst.GetOpcode())));
|
|
}
|
|
if (fixed_signature && arg.GetType() != ArgTypeOf(inst.GetOpcode(), arg_index)) {
|
|
return Fail(fmt::format("{} argument {} has type {}, expected {}",
|
|
ValueOpcodeName(inst.GetOpcode()), arg_index,
|
|
TypeName(arg.GetType()),
|
|
TypeName(ArgTypeOf(inst.GetOpcode(), arg_index))));
|
|
}
|
|
if (const auto* definition = arg.TryInstruction();
|
|
definition != nullptr && !instruction_positions.contains(definition)) {
|
|
return Fail("value IR argument has a foreign definition");
|
|
}
|
|
if (const auto* definition = arg.TryInstruction(); definition != nullptr) {
|
|
const auto& uses = definition->Uses();
|
|
const auto use = std::ranges::find_if(uses, [&](const Use& candidate) {
|
|
return candidate.user == &inst && candidate.operand == arg_index;
|
|
});
|
|
if (use == uses.end()) {
|
|
return Fail(fmt::format("{} argument {} is absent from {} reverse uses",
|
|
ValueOpcodeName(inst.GetOpcode()), arg_index,
|
|
ValueOpcodeName(definition->GetOpcode())));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (program.blocks.empty()) {
|
|
return;
|
|
}
|
|
|
|
std::vector<bool> reachable(program.blocks.size(), false);
|
|
std::vector<size_t> pending {0u};
|
|
while (!pending.empty()) {
|
|
const auto block_index = pending.back();
|
|
pending.pop_back();
|
|
if (reachable[block_index]) {
|
|
continue;
|
|
}
|
|
reachable[block_index] = true;
|
|
for (const auto* successor: program.blocks[block_index]->ImmSuccessors()) {
|
|
pending.push_back(block_indices.at(successor));
|
|
}
|
|
}
|
|
if (!std::ranges::all_of(reachable, [](bool value) { return value; })) {
|
|
return Fail("value IR contains an unreachable block");
|
|
}
|
|
|
|
std::vector<std::vector<bool>> dominators(program.blocks.size(),
|
|
std::vector<bool>(program.blocks.size(), true));
|
|
dominators.front().assign(program.blocks.size(), false);
|
|
dominators.front().front() = true;
|
|
bool changed = true;
|
|
while (changed) {
|
|
changed = false;
|
|
for (size_t block_index = 1; block_index < program.blocks.size(); block_index++) {
|
|
std::vector<bool> next(program.blocks.size(), true);
|
|
for (const auto* predecessor: program.blocks[block_index]->ImmPredecessors()) {
|
|
const auto predecessor_index = block_indices.at(predecessor);
|
|
for (size_t candidate = 0; candidate < next.size(); candidate++) {
|
|
next[candidate] = next[candidate] && dominators[predecessor_index][candidate];
|
|
}
|
|
}
|
|
next[block_index] = true;
|
|
if (next != dominators[block_index]) {
|
|
dominators[block_index] = std::move(next);
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
const auto dominates = [&](const Block* definition, const Block* use) {
|
|
return dominators[block_indices.at(use)][block_indices.at(definition)];
|
|
};
|
|
const auto control_dominates = [&](Value value, const Block* use) {
|
|
const auto* definition = value.TryInstruction();
|
|
return definition == nullptr || definition->Parent() == use ||
|
|
dominates(definition->Parent(), use);
|
|
};
|
|
|
|
for (size_t block_index = 0; block_index < program.blocks.size(); block_index++) {
|
|
const auto* block = program.blocks[block_index];
|
|
for (const auto& inst: *block) {
|
|
for (size_t arg_index = 0; arg_index < inst.NumArgs(); arg_index++) {
|
|
const auto* definition = inst.Arg(arg_index).TryInstruction();
|
|
if (definition == nullptr) {
|
|
continue;
|
|
}
|
|
if (inst.GetOpcode() == ValueOpcode::Phi) {
|
|
const auto* predecessor = inst.PhiBlock(arg_index);
|
|
if (definition->Parent() != predecessor &&
|
|
!dominates(definition->Parent(), predecessor)) {
|
|
return Fail("value IR Phi incoming definition does not dominate its edge");
|
|
}
|
|
} else if (definition->Parent() == block) {
|
|
if (instruction_positions.at(definition) >= instruction_positions.at(&inst)) {
|
|
return Fail("value IR instruction uses a same-block definition before it");
|
|
}
|
|
} else if (!dominates(definition->Parent(), block)) {
|
|
return Fail("value IR instruction definition does not dominate its use");
|
|
}
|
|
}
|
|
}
|
|
const auto& info = *program.blocks[block_index];
|
|
if (!info.condition.IsEmpty() && !control_dominates(info.condition, block)) {
|
|
return Fail("value IR branch condition definition does not dominate its use");
|
|
}
|
|
if (!info.indirect_target.IsEmpty() && !control_dominates(info.indirect_target, block)) {
|
|
return Fail("value IR indirect selector definition does not dominate its use");
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string ProgramToString(const Program& program) {
|
|
std::map<const Inst*, size_t> ids;
|
|
size_t next_id = 1;
|
|
for (const auto* block: program.blocks) {
|
|
for (const auto& inst: *block) {
|
|
ids.emplace(&inst, next_id++);
|
|
}
|
|
}
|
|
const auto value_string = [&](Value value) {
|
|
if (value.IsEmpty()) {
|
|
return std::string("<null>");
|
|
}
|
|
if (const auto* inst = value.TryInstruction(); inst != nullptr) {
|
|
return fmt::format("%{}", ids.at(inst));
|
|
}
|
|
switch (value.GetType()) {
|
|
case Type::ScalarReg: return fmt::format("s{}", RegIndex(value.ScalarRegister()));
|
|
case Type::VectorReg: return fmt::format("v{}", RegIndex(value.VectorRegister()));
|
|
case Type::U1: return std::string(value.U1() ? "true" : "false");
|
|
case Type::U8: return fmt::format("{}u8", value.U8());
|
|
case Type::U16: return fmt::format("{}u16", value.U16());
|
|
case Type::U32: return fmt::format("0x{:08x}", value.U32());
|
|
case Type::U64: return fmt::format("0x{:016x}", value.U64());
|
|
case Type::F16: return fmt::format("f16(0x{:04x})", value.F16Bits());
|
|
case Type::F32: return fmt::format("{}f", value.F32Value());
|
|
default: return fmt::format("<{}>", TypeName(value.GetType()));
|
|
}
|
|
};
|
|
|
|
std::string text;
|
|
for (size_t block_index = 0; block_index < program.blocks.size(); block_index++) {
|
|
text +=
|
|
fmt::format("Block ${} pc=0x{:08x}..0x{:08x}\n", block_index,
|
|
program.blocks[block_index]->start_pc, program.blocks[block_index]->end_pc);
|
|
for (const auto& inst: *program.blocks[block_index]) {
|
|
const auto type = inst.GetType();
|
|
if (type != Type::Void) {
|
|
text +=
|
|
fmt::format(" %{:<5} = {}", ids.at(&inst), ValueOpcodeName(inst.GetOpcode()));
|
|
} else {
|
|
text += fmt::format(" {}", ValueOpcodeName(inst.GetOpcode()));
|
|
}
|
|
for (size_t index = 0; index < inst.NumArgs(); index++) {
|
|
text += index == 0 ? " " : ", ";
|
|
if (inst.GetOpcode() == ValueOpcode::Phi) {
|
|
const auto predecessor =
|
|
std::ranges::find(program.blocks, inst.PhiBlock(index));
|
|
text += fmt::format("[{}, ${}]", value_string(inst.Arg(index)),
|
|
std::distance(program.blocks.begin(), predecessor));
|
|
} else {
|
|
text += value_string(inst.Arg(index));
|
|
}
|
|
}
|
|
text +=
|
|
fmt::format(" ({}; uses={})\n", TypeName(Value(const_cast<Inst*>(&inst)).GetType()),
|
|
inst.UseCount());
|
|
}
|
|
}
|
|
return text;
|
|
}
|
|
|
|
} // namespace Libs::Graphics::ShaderRecompiler::IR
|