#include "common/assert.h" #include "graphics/shader/recompiler/ir/ShaderIR.h" #include "graphics/shader/recompiler/BufferFormat.h" #include #include #include #include #include #include #include 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>& 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().index; const auto ri = rhs->Flags().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() != rhs->Flags()) { 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> 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 pending {value}; std::unordered_set 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 block_indices; std::unordered_set block_ids; std::unordered_map 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 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 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 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 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 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().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().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().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().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().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().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 reachable(program.blocks.size(), false); std::vector 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> dominators(program.blocks.size(), std::vector(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 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 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(""); } 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)).GetType()), inst.UseCount()); } } return text; } } // namespace Libs::Graphics::ShaderRecompiler::IR