Files
KytyPS5/src/graphics/shader/recompiler/ir/Program.cpp
T

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