#include "graphics/shader/recompiler/frontend/cfg/ShaderCFG.h" #include "common/assert.h" #include #include #include #include #include #include #include namespace Libs::Graphics::ShaderRecompiler::CFG { namespace { using Decoder::Instruction; using Decoder::Opcode; struct SetpcTargetInfo { uint32_t target = 0; bool indirect = false; uint32_t pc_sgpr = UINT32_MAX; uint32_t selector_code = UINT32_MAX; uint32_t table_load_pc = UINT32_MAX; std::vector target_pcs; std::vector selector_values; std::vector selector_target_pcs; }; void SetFailure(Graph& graph, FailureKind kind, uint32_t block_id, const std::string& message) { graph.unsupported = true; graph.failure_kind = kind; graph.failure_block = block_id; graph.unsupported_reason = message; } [[noreturn]] void ExitBuildFailure(Graph& graph, FailureKind kind, uint32_t block_id, const std::string& message) { SetFailure(graph, kind, block_id, message); EXIT("shader CFG build failed: %s", message.c_str()); std::abort(); } uint32_t InstructionEndPc(const Instruction& inst) { return inst.pc + inst.word_count * 4u; } uint32_t ProgramEndPc(const Decoder::Program& program) { if (program.instructions.empty()) { return 0; } return InstructionEndPc(program.instructions.back()); } BranchCondition ConditionForOpcode(Opcode opcode) { switch (opcode) { case Opcode::S_BRANCH: return BranchCondition::Always; case Opcode::S_CBRANCH_SCC0: return BranchCondition::SccZero; case Opcode::S_CBRANCH_SCC1: return BranchCondition::SccNonZero; case Opcode::S_CBRANCH_VCCZ: return BranchCondition::VccZero; case Opcode::S_CBRANCH_VCCNZ: return BranchCondition::VccNonZero; case Opcode::S_CBRANCH_EXECZ: return BranchCondition::ExecZero; case Opcode::S_CBRANCH_EXECNZ: return BranchCondition::ExecNonZero; case Opcode::S_SUBVECTOR_LOOP_BEGIN: case Opcode::S_SUBVECTOR_LOOP_END: return BranchCondition::ScalarInstruction; default: return BranchCondition::Unknown; } } bool IsRegister(const Decoder::Operand& operand, Decoder::OperandKind kind, uint32_t reg) { return operand.kind == kind && operand.reg == reg; } bool ScalarOperandCode(const Decoder::Operand& operand, uint32_t& code) { switch (operand.kind) { case Decoder::OperandKind::Sgpr: code = operand.reg; return true; case Decoder::OperandKind::VccLo: code = 106u; return true; case Decoder::OperandKind::VccHi: code = 107u; return true; case Decoder::OperandKind::M0: code = 124u; return true; case Decoder::OperandKind::ExecLo: code = 126u; return true; case Decoder::OperandKind::ExecHi: code = 127u; return true; default: return false; } } bool IsScalarCode(const Decoder::Operand& operand, uint32_t code) { uint32_t operand_code = 0; return ScalarOperandCode(operand, operand_code) && operand_code == code; } bool IsImmediate(const Decoder::Operand& operand, uint32_t& value) { if (operand.kind == Decoder::OperandKind::IntegerInlineConstant || operand.kind == Decoder::OperandKind::LiteralConstant || operand.kind == Decoder::OperandKind::FloatInlineConstant) { value = operand.value; return true; } return false; } bool IsImmediateSigned(const Decoder::Operand& operand, int32_t& value) { if (operand.kind == Decoder::OperandKind::IntegerInlineConstant || operand.kind == Decoder::OperandKind::LiteralConstant) { value = operand.signed_val; return true; } if (operand.kind == Decoder::OperandKind::FloatInlineConstant) { value = static_cast(operand.value); return true; } return false; } bool OtherScalarSource(const Instruction& inst, uint32_t known_code, const Decoder::Operand** other) { if (other == nullptr) { return false; } if (IsScalarCode(inst.src0, known_code)) { *other = &inst.src1; return true; } if (IsScalarCode(inst.src1, known_code)) { *other = &inst.src0; return true; } return false; } bool InstructionWritesScalarCode(const Instruction& inst, uint32_t code) { uint32_t dst_code = 0; if (!ScalarOperandCode(inst.dst, dst_code)) { return false; } const uint32_t count = std::max(1u, inst.data_dwords); return code >= dst_code && code < dst_code + count; } bool ScalarCodeWrittenInRange(const Decoder::Program& program, uint32_t begin_index, uint32_t end_index, uint32_t code); bool FindPreviousGetpc(const Decoder::Program& program, uint32_t before_index, uint32_t dst_code, uint32_t* index) { for (uint32_t i = before_index; i > 0; i--) { const uint32_t candidate_index = i - 1u; const auto& candidate = program.instructions[candidate_index]; if (candidate.opcode == Opcode::S_GETPC_B64 && IsScalarCode(candidate.dst, dst_code)) { if (index != nullptr) { *index = candidate_index; } return true; } if (InstructionWritesScalarCode(candidate, dst_code)) { return false; } } return false; } bool ResolvePcRelativeBase(const Decoder::Program& program, uint32_t before_index, uint32_t base_code, uint32_t* pc) { if (pc == nullptr) { return false; } for (uint32_t i = before_index; i > 0; i--) { const uint32_t candidate_index = i - 1u; const auto& candidate = program.instructions[candidate_index]; if (!InstructionWritesScalarCode(candidate, base_code)) { continue; } const bool adds = candidate.opcode == Opcode::S_ADD_U32 || candidate.opcode == Opcode::S_ADD_I32; const bool subs = candidate.opcode == Opcode::S_SUB_U32 || candidate.opcode == Opcode::S_SUB_I32; if (!adds && !subs) { return false; } const Decoder::Operand* offset = nullptr; int32_t imm = 0; uint32_t getpc_index = 0; if (adds) { if (!OtherScalarSource(candidate, base_code, &offset)) { return false; } } else { if (!IsScalarCode(candidate.src0, base_code)) { return false; } offset = &candidate.src1; } if (!IsImmediateSigned(*offset, imm) || !FindPreviousGetpc(program, candidate_index, base_code, &getpc_index)) { return false; } if (candidate_index + 1u >= before_index) { return false; } const auto& high = program.instructions[candidate_index + 1u]; const Decoder::Operand* high_other = nullptr; uint32_t zero = 0; if (high.opcode != (adds ? Opcode::S_ADDC_U32 : Opcode::S_SUBB_U32) || !IsScalarCode(high.dst, base_code + 1u)) { return false; } if (adds) { if (!OtherScalarSource(high, base_code + 1u, &high_other)) { return false; } } else { if (!IsScalarCode(high.src0, base_code + 1u)) { return false; } high_other = &high.src1; } if (!IsImmediate(*high_other, zero) || zero != 0u || ScalarCodeWrittenInRange(program, candidate_index + 2u, before_index, base_code + 1u)) { return false; } const auto base = InstructionEndPc(program.instructions[getpc_index]); *pc = adds ? base + static_cast(imm) : base - static_cast(imm); *pc &= ~3u; return true; } return false; } bool AddSignedByteOffset(uint32_t base, uint32_t encoded_offset, uint32_t* address) { const auto value = static_cast(base) + static_cast(encoded_offset); if (address == nullptr || value < 0 || value > UINT32_MAX) { return false; } *address = static_cast(value); return true; } bool FindPreviousScalarLoad(const Decoder::Program& program, uint32_t before_index, uint32_t dst_code, Opcode opcode, uint32_t* index) { for (uint32_t i = before_index; i > 0; i--) { const uint32_t candidate_index = i - 1u; const auto& candidate = program.instructions[candidate_index]; if (InstructionWritesScalarCode(candidate, dst_code)) { if (candidate.opcode == opcode && IsScalarCode(candidate.dst, dst_code)) { if (index != nullptr) { *index = candidate_index; } return true; } return false; } } return false; } bool ResolveJumpTableEntryCount(const Decoder::Program& program, uint32_t before_index, const Decoder::Operand& byte_offset_operand, uint32_t stride_shift, uint32_t* entry_count) { if (entry_count == nullptr) { return false; } uint32_t byte_offset_code = 0; if (!ScalarOperandCode(byte_offset_operand, byte_offset_code)) { return false; } for (uint32_t i = before_index; i > 0; i--) { const uint32_t shift_index = i - 1u; const auto& shift = program.instructions[shift_index]; if (!InstructionWritesScalarCode(shift, byte_offset_code)) { continue; } if (shift.opcode != Opcode::S_LSHL_B32 || !IsScalarCode(shift.dst, byte_offset_code)) { return false; } const Decoder::Operand* shift_amount_operand = nullptr; uint32_t shift_amount = 0; if (!OtherScalarSource(shift, byte_offset_code, &shift_amount_operand) || !IsImmediate(*shift_amount_operand, shift_amount) || shift_amount != stride_shift) { return false; } uint32_t index_code = byte_offset_code; for (uint32_t j = shift_index; j > 0; j--) { const uint32_t clamp_index = j - 1u; const auto& clamp = program.instructions[clamp_index]; if (!InstructionWritesScalarCode(clamp, index_code)) { continue; } if (clamp.opcode != Opcode::S_MIN_U32 || !IsScalarCode(clamp.dst, index_code)) { return false; } uint32_t max_index = 0; if (!IsImmediate(clamp.src0, max_index) && !IsImmediate(clamp.src1, max_index)) { return false; } *entry_count = max_index + 1u; return *entry_count != 0; } return false; } return false; } void AddUnique(std::vector& values, uint32_t value) { if (std::find(values.begin(), values.end(), value) == values.end()) { values.push_back(value); } } bool ScalarCodeWrittenInRange(const Decoder::Program& program, uint32_t begin_index, uint32_t end_index, uint32_t code) { const auto end = std::min(end_index, static_cast(program.instructions.size())); for (uint32_t i = begin_index; i < end; i++) { if (InstructionWritesScalarCode(program.instructions[i], code)) { return true; } } return false; } bool ResolveSetpcJumpTable(const Decoder::Program& program, uint32_t setpc_index, SetpcTargetInfo& info) { if (setpc_index < 4u || setpc_index >= program.instructions.size()) { return false; } const auto& setpc = program.instructions[setpc_index]; uint32_t pc_reg = 0; if (setpc.opcode != Opcode::S_SETPC_B64 || !ScalarOperandCode(setpc.src0, pc_reg) || setpc.src0.kind != Decoder::OperandKind::Sgpr) { return false; } const auto& getpc = program.instructions[setpc_index - 3u]; const auto& add = program.instructions[setpc_index - 2u]; const auto& addc = program.instructions[setpc_index - 1u]; if (getpc.opcode != Opcode::S_GETPC_B64 || !IsScalarCode(getpc.dst, pc_reg) || add.opcode != Opcode::S_ADD_U32 || !IsScalarCode(add.dst, pc_reg) || addc.opcode != Opcode::S_ADDC_U32 || !IsScalarCode(addc.dst, pc_reg + 1u)) { return false; } const Decoder::Operand* offset_low_operand = nullptr; const Decoder::Operand* offset_high_operand = nullptr; uint32_t offset_low_code = 0; uint32_t offset_high_code = 0; if (!OtherScalarSource(add, pc_reg, &offset_low_operand) || !OtherScalarSource(addc, pc_reg + 1u, &offset_high_operand) || !ScalarOperandCode(*offset_low_operand, offset_low_code) || !ScalarOperandCode(*offset_high_operand, offset_high_code) || offset_high_code != offset_low_code + 1u) { return false; } uint32_t load_index = 0; if (!FindPreviousScalarLoad(program, setpc_index - 3u, offset_low_code, Opcode::S_LOAD_DWORDX2, &load_index)) { return false; } const auto& load = program.instructions[load_index]; uint32_t table_base_code = 0; if (!ScalarOperandCode(load.src0, table_base_code)) { return false; } uint32_t table_pc = 0; if (!ResolvePcRelativeBase(program, load_index, table_base_code, &table_pc) || !AddSignedByteOffset(table_pc, load.offset, &table_pc)) { return false; } uint32_t entry_count = 0; if (!ResolveJumpTableEntryCount(program, load_index, load.src1, 3u, &entry_count)) { return false; } uint32_t selector_code = UINT32_MAX; if (!ScalarOperandCode(load.src1, selector_code) || ScalarCodeWrittenInRange(program, load_index + 1u, setpc_index, selector_code)) { return false; } const uint32_t target_base = InstructionEndPc(getpc); const size_t table_word = table_pc / 4u; const size_t table_words = static_cast(entry_count) * 2u; if ((table_pc & 3u) != 0 || table_word > program.code.size() || table_words > program.code.size() - table_word) { return false; } std::vector targets; std::vector selector_values; std::vector selector_target_pcs; for (uint32_t i = 0; i < entry_count; i++) { const uint32_t low = program.code[table_word + i * 2u]; const uint32_t high = program.code[table_word + i * 2u + 1u]; if (high != 0u && high != UINT32_MAX) { return false; } const auto target_pc = (target_base + low) & ~3u; AddUnique(targets, target_pc); selector_values.push_back(i * 8u); selector_target_pcs.push_back(target_pc); } if (targets.empty()) { return false; } info.indirect = true; info.pc_sgpr = pc_reg; info.selector_code = selector_code; info.table_load_pc = load.pc; info.target_pcs = std::move(targets); info.selector_values = std::move(selector_values); info.selector_target_pcs = std::move(selector_target_pcs); return true; } bool ResolveSetpcDwordJumpTable(const Decoder::Program& program, uint32_t setpc_index, SetpcTargetInfo& info) { if (setpc_index < 3u || setpc_index >= program.instructions.size()) { return false; } const auto& setpc = program.instructions[setpc_index]; uint32_t pc_reg = 0; if (setpc.opcode != Opcode::S_SETPC_B64 || setpc.src0.kind != Decoder::OperandKind::Sgpr || !ScalarOperandCode(setpc.src0, pc_reg)) { return false; } const auto& low_sub = program.instructions[setpc_index - 2u]; const auto& high_sub = program.instructions[setpc_index - 1u]; uint32_t offset_code = 0; uint32_t zero = 0; if (low_sub.opcode != Opcode::S_SUB_U32 || !IsScalarCode(low_sub.dst, pc_reg) || !IsScalarCode(low_sub.src0, pc_reg) || !ScalarOperandCode(low_sub.src1, offset_code) || high_sub.opcode != Opcode::S_SUBB_U32 || !IsScalarCode(high_sub.dst, pc_reg + 1u) || !IsScalarCode(high_sub.src0, pc_reg + 1u) || !IsImmediate(high_sub.src1, zero) || zero != 0u || offset_code == pc_reg || offset_code == pc_reg + 1u) { return false; } uint32_t load_index = 0; if (!FindPreviousScalarLoad(program, setpc_index - 2u, offset_code, Opcode::S_LOAD_DWORD, &load_index)) { return false; } const auto& load = program.instructions[load_index]; if (!IsScalarCode(load.src0, pc_reg)) { return false; } uint32_t target_base = 0; if (!ResolvePcRelativeBase(program, load_index, pc_reg, &target_base)) { return false; } uint32_t table_pc = 0; if (!AddSignedByteOffset(target_base, load.offset, &table_pc)) { return false; } uint32_t entry_count = 0; if (!ResolveJumpTableEntryCount(program, load_index, load.src1, 2u, &entry_count)) { return false; } uint32_t selector_code = UINT32_MAX; if (!ScalarOperandCode(load.src1, selector_code) || selector_code == offset_code || ScalarCodeWrittenInRange(program, load_index + 1u, setpc_index, selector_code)) { return false; } const size_t table_word = table_pc / 4u; if ((table_pc & 3u) != 0 || table_word > program.code.size() || entry_count > program.code.size() - table_word) { return false; } std::vector targets; std::vector selector_values; std::vector selector_target_pcs; for (uint32_t i = 0; i < entry_count; i++) { const uint32_t offset = program.code[table_word + i]; if (offset > target_base) { return false; } const auto target_pc = (target_base - offset) & ~3u; AddUnique(targets, target_pc); selector_values.push_back(i * 4u); selector_target_pcs.push_back(target_pc); } if (targets.empty()) { return false; } info.indirect = true; info.pc_sgpr = pc_reg; info.selector_code = selector_code; info.table_load_pc = load.pc; info.target_pcs = std::move(targets); info.selector_values = std::move(selector_values); info.selector_target_pcs = std::move(selector_target_pcs); return true; } bool ResolveSetpcTarget(const Decoder::Program& program, uint32_t setpc_index, uint32_t& target) { if (setpc_index >= program.instructions.size()) { return false; } const auto& setpc = program.instructions[setpc_index]; if (setpc.opcode != Opcode::S_SETPC_B64 || setpc.src0.kind != Decoder::OperandKind::Sgpr) { return false; } const auto pc_reg = setpc.src0.reg; if (setpc_index >= 2u) { const auto& arith = program.instructions[setpc_index - 1u]; const auto& getpc = program.instructions[setpc_index - 2u]; if (getpc.opcode == Opcode::S_GETPC_B64 && getpc.dst.kind == Decoder::OperandKind::Sgpr && getpc.dst.reg == pc_reg && arith.dst.kind == Decoder::OperandKind::Sgpr && arith.dst.reg == pc_reg) { uint32_t imm = 0; bool adds = arith.opcode == Opcode::S_ADD_U32 || arith.opcode == Opcode::S_ADD_I32; bool subs = arith.opcode == Opcode::S_SUB_U32 || arith.opcode == Opcode::S_SUB_I32; if ((adds || subs) && (IsRegister(arith.src0, Decoder::OperandKind::Sgpr, pc_reg) || IsRegister(arith.src1, Decoder::OperandKind::Sgpr, pc_reg)) && (IsImmediate(arith.src0, imm) || IsImmediate(arith.src1, imm))) { const auto base = InstructionEndPc(getpc); target = (adds ? base + imm : base - imm) & ~3u; return true; } } } if (setpc_index >= 1u) { const auto& getpc = program.instructions[setpc_index - 1u]; if (getpc.opcode == Opcode::S_GETPC_B64 && getpc.dst.kind == Decoder::OperandKind::Sgpr && getpc.dst.reg == pc_reg) { target = InstructionEndPc(getpc); return true; } } return false; } bool ResolveSetpcTargets(const Decoder::Program& program, uint32_t setpc_index, SetpcTargetInfo& info) { info = {}; if (ResolveSetpcDwordJumpTable(program, setpc_index, info)) { return true; } if (ResolveSetpcJumpTable(program, setpc_index, info)) { return true; } uint32_t target = 0; if (ResolveSetpcTarget(program, setpc_index, target)) { info.target = target; return true; } return false; } bool IsValidTarget(uint32_t target, std::span instructions, uint32_t end_pc) { return target == end_pc || std::ranges::binary_search(instructions, target, {}, &Instruction::pc); } std::vector AllBlockIds(uint32_t count) { std::vector ids; ids.reserve(count); for (uint32_t i = 0; i < count; i++) { ids.push_back(i); } return ids; } std::vector IntersectSorted(const std::vector& a, const std::vector& b) { std::vector ret; ret.reserve(std::min(a.size(), b.size())); std::set_intersection(a.begin(), a.end(), b.begin(), b.end(), std::back_inserter(ret)); return ret; } void SortUnique(std::vector& values) { std::sort(values.begin(), values.end()); values.erase(std::unique(values.begin(), values.end()), values.end()); } bool Contains(const std::vector& values, uint32_t value) { return std::find(values.begin(), values.end(), value) != values.end(); } uint32_t RemapId(uint32_t id, const std::vector& id_map) { return id != UINT32_MAX && id < id_map.size() ? id_map[id] : id; } void RemapIds(std::vector& values, const std::vector& id_map) { for (auto& value: values) { value = RemapId(value, id_map); } SortUnique(values); } void RebuildPredecessors(Graph& graph) { for (auto& block: graph.blocks) { block.predecessors.clear(); SortUnique(block.successors); } for (const auto& block: graph.blocks) { for (auto succ: block.successors) { if (succ < graph.blocks.size()) { AddUnique(graph.blocks[succ].predecessors, block.id); } } } for (auto& block: graph.blocks) { SortUnique(block.predecessors); } } void PruneUnreachableBlocks(Graph& graph) { if (graph.entry_block >= graph.blocks.size()) { return; } std::vector reachable(graph.blocks.size(), false); std::vector pending {graph.entry_block}; while (!pending.empty()) { const auto block_id = pending.back(); pending.pop_back(); if (block_id >= graph.blocks.size() || reachable[block_id]) { continue; } reachable[block_id] = true; for (const auto successor: graph.blocks[block_id].successors) { pending.push_back(successor); } } if (std::ranges::all_of(reachable, [](bool value) { return value; })) { return; } std::vector id_map(graph.blocks.size(), UINT32_MAX); std::vector blocks; blocks.reserve(std::ranges::count(reachable, true)); for (uint32_t old_id = 0; old_id < graph.blocks.size(); old_id++) { if (!reachable[old_id]) { continue; } id_map[old_id] = static_cast(blocks.size()); blocks.push_back(std::move(graph.blocks[old_id])); } graph.entry_block = RemapId(graph.entry_block, id_map); graph.failure_block = RemapId(graph.failure_block, id_map); graph.blocks = std::move(blocks); for (auto& block: graph.blocks) { block.id = RemapId(block.id, id_map); RemapIds(block.successors, id_map); block.predecessors.clear(); block.dominators.clear(); auto& terminator = block.terminator; terminator.true_block = RemapId(terminator.true_block, id_map); terminator.false_block = RemapId(terminator.false_block, id_map); terminator.merge_block = RemapId(terminator.merge_block, id_map); terminator.continue_block = RemapId(terminator.continue_block, id_map); for (auto& target: terminator.indirect_targets) { target = RemapId(target, id_map); } for (auto& target: terminator.indirect_selector_targets) { target = RemapId(target, id_map); } } graph.back_edges.clear(); graph.natural_loops.clear(); graph.components.clear(); RebuildPredecessors(graph); } void ComputeDominators(Graph& graph) { const auto count = static_cast(graph.blocks.size()); const auto all = AllBlockIds(count); for (auto& block: graph.blocks) { block.dominators = (block.id == graph.entry_block ? std::vector {block.id} : all); } bool changed = true; while (changed) { changed = false; for (auto& block: graph.blocks) { if (block.id == graph.entry_block) { continue; } std::vector next; if (block.predecessors.empty()) { next = {block.id}; } else { next = graph.blocks[block.predecessors.front()].dominators; for (uint32_t i = 1; i < block.predecessors.size(); i++) { next = IntersectSorted(next, graph.blocks[block.predecessors[i]].dominators); } AddUnique(next, block.id); SortUnique(next); } if (next != block.dominators) { block.dominators = std::move(next); changed = true; } } } } void ComputeBackEdges(Graph& graph) { graph.back_edges.clear(); for (const auto& block: graph.blocks) { for (auto succ: block.successors) { if (graph.Dominates(succ, block.id)) { graph.back_edges.push_back({block.id, succ, true}); } } } } std::vector NaturalLoopBody(const Graph& graph, uint32_t header, uint32_t latch, bool* natural) { std::vector body; std::vector stack; body.reserve(graph.blocks.size()); stack.reserve(graph.blocks.size()); AddUnique(body, header); AddUnique(body, latch); if (latch != header) { stack.push_back(latch); } if (natural != nullptr) { *natural = true; } while (!stack.empty()) { const auto block_id = stack.back(); stack.pop_back(); const auto* block = graph.FindBlock(block_id); if (block == nullptr) { continue; } for (auto pred: block->predecessors) { if (!graph.Dominates(header, pred) && natural != nullptr) { *natural = false; } if (!Contains(body, pred)) { body.push_back(pred); if (pred != header) { stack.push_back(pred); } } } } SortUnique(body); return body; } void ComputeNaturalLoops(Graph& graph) { graph.natural_loops.clear(); for (auto& edge: graph.back_edges) { bool natural = true; auto body = NaturalLoopBody(graph, edge.to, edge.from, &natural); edge.natural = natural; NaturalLoop loop; loop.header = edge.to; loop.latch = edge.from; loop.body_blocks = std::move(body); graph.natural_loops.push_back(std::move(loop)); } } struct TarjanState { const Graph* graph = nullptr; uint32_t next_index = 0; std::vector index; std::vector lowlink; std::vector on_stack; std::vector stack; std::vector components; }; void TarjanVisit(TarjanState& state, uint32_t block_id) { state.index[block_id] = state.next_index; state.lowlink[block_id] = state.next_index; state.next_index++; state.stack.push_back(block_id); state.on_stack[block_id] = true; const auto* block = state.graph->FindBlock(block_id); if (block != nullptr) { for (auto succ: block->successors) { if (state.index[succ] == UINT32_MAX) { TarjanVisit(state, succ); state.lowlink[block_id] = std::min(state.lowlink[block_id], state.lowlink[succ]); } else if (state.on_stack[succ]) { state.lowlink[block_id] = std::min(state.lowlink[block_id], state.index[succ]); } } } if (state.lowlink[block_id] != state.index[block_id]) { return; } StronglyConnectedComponent component; for (;;) { const auto member = state.stack.back(); state.stack.pop_back(); state.on_stack[member] = false; component.blocks.push_back(member); if (member == block_id) { break; } } SortUnique(component.blocks); bool cyclic = component.blocks.size() > 1u; for (auto member: component.blocks) { const auto* member_block = state.graph->FindBlock(member); if (member_block != nullptr && Contains(member_block->successors, member)) { cyclic = true; } if (member_block != nullptr) { for (auto pred: member_block->predecessors) { if (!Contains(component.blocks, pred)) { AddUnique(component.entry_blocks, member); } } } } SortUnique(component.entry_blocks); component.irreducible = cyclic && component.entry_blocks.size() > 1u; state.components.push_back(std::move(component)); } void ComputeComponents(Graph& graph) { TarjanState state; state.graph = &graph; state.index.assign(graph.blocks.size(), UINT32_MAX); state.lowlink.assign(graph.blocks.size(), UINT32_MAX); state.on_stack.assign(graph.blocks.size(), false); state.stack.reserve(graph.blocks.size()); state.components.reserve(graph.blocks.size()); for (const auto& block: graph.blocks) { if (state.index[block.id] == UINT32_MAX) { TarjanVisit(state, block.id); } } graph.components = std::move(state.components); graph.irreducible = false; for (const auto& component: graph.components) { if (component.irreducible) { graph.irreducible = true; graph.failure_kind = FailureKind::IrreducibleControlFlow; graph.failure_block = component.entry_blocks.empty() ? component.blocks.front() : component.entry_blocks.front(); graph.unsupported_reason = "irreducible CFG: cyclic component has multiple entries"; break; } } } void RecomputeAnalyses(Graph& graph) { ComputeDominators(graph); ComputeBackEdges(graph); ComputeNaturalLoops(graph); ComputeComponents(graph); } std::vector ApplyBlockOrder(Graph& graph, std::vector blocks) { std::vector id_map(blocks.size(), UINT32_MAX); for (uint32_t i = 0; i < blocks.size(); i++) { id_map[blocks[i].id] = i; } graph.blocks = std::move(blocks); graph.entry_block = RemapId(graph.entry_block, id_map); for (auto& block: graph.blocks) { block.id = RemapId(block.id, id_map); RemapIds(block.predecessors, id_map); RemapIds(block.successors, id_map); RemapIds(block.dominators, id_map); block.terminator.true_block = RemapId(block.terminator.true_block, id_map); block.terminator.false_block = RemapId(block.terminator.false_block, id_map); block.terminator.merge_block = RemapId(block.terminator.merge_block, id_map); block.terminator.continue_block = RemapId(block.terminator.continue_block, id_map); } return id_map; } std::string VectorToString(const std::vector& values) { std::string text; for (uint32_t i = 0; i < values.size(); i++) { if (i != 0) { text += ","; } text += fmt::format("{}", values[i]); } return text; } } // namespace const BasicBlock* Graph::FindBlock(uint32_t id) const { if (id < blocks.size() && blocks[id].id == id) { return &blocks[id]; } for (const auto& block: blocks) { if (block.id == id) { return █ } } return nullptr; } BasicBlock* Graph::FindBlock(uint32_t id) { return const_cast(static_cast(this)->FindBlock(id)); } const BasicBlock* Graph::FindBlockByPc(uint32_t pc) const { for (const auto& block: blocks) { if (block.start_pc == pc) { return █ } } return nullptr; } BasicBlock* Graph::FindBlockByPc(uint32_t pc) { return const_cast(static_cast(this)->FindBlockByPc(pc)); } bool Graph::Dominates(uint32_t dominator, uint32_t block) const { const auto* target = FindBlock(block); return target != nullptr && Contains(target->dominators, dominator); } namespace { // Eliminate gotos by moving them through a structured statement tree. Every Code node // retains exactly one guest block; only Boolean route values cross construct boundaries. // This follows the outward/inward/lifting transformations used by shadPS4's GotoPass. class GotoStructurizer { using ExprOp = ConditionExpression::Op; enum class Kind { Root, Code, Label, Goto, If, Loop, Break, Return, Set }; struct Node; using List = std::list; struct Node { Kind kind; Node* parent = nullptr; Node* target = nullptr; List children; List::iterator position; uint32_t condition = 1; uint32_t id = UINT32_MAX; }; public: explicit GotoStructurizer(const Graph& source): m_source(source) { m_graph.expressions = {{ExprOp::Constant, 0}, {ExprOp::Constant, 1}}; m_graph.code_table_load_pcs = source.code_table_load_pcs; m_root = New(Kind::Root); std::vector labels; for (const auto& block: source.blocks) { auto* label = Insert(m_root, m_root->children.end(), Kind::Label); label->id = block.id; labels.push_back(label); } // Give every natural loop one backedge before eliminating gotos. Otherwise // multiple latches become nested loops that retest the same exit predicate. struct Continue { uint32_t header; uint32_t after; size_t body_size; Node* label; }; std::vector continues; for (const auto& loop: source.natural_loops) { if (std::ranges::any_of(continues, [&](const auto& value) { return value.header == loop.header; }) || std::ranges::count(source.natural_loops, loop.header, &NaturalLoop::header) < 2) continue; std::vector body; for (const auto& member: source.natural_loops) { if (member.header == loop.header) body.insert(body.end(), member.body_blocks.begin(), member.body_blocks.end()); } SortUnique(body); auto* label = New(Kind::Label); label->id = static_cast(labels.size()); labels.push_back(label); continues.push_back({loop.header, body.back(), body.size(), label}); } std::ranges::sort(continues, [](const auto& a, const auto& b) { return a.after != b.after ? a.after < b.after : a.body_size < b.body_size; }); m_route_expressions.resize(labels.size(), UINT32_MAX); for (const auto& block: source.blocks) { auto* label = labels.at(block.id); const auto end = std::next(label->position); auto* code = Insert(m_root, end, Kind::Code); code->id = block.id; const auto add_goto = [&](uint32_t condition, uint32_t target) { auto* node = Insert(m_root, end, Kind::Goto); node->condition = condition; node->target = labels.at(target); for (const auto& value: continues) { if (value.header == target && source.Dominates(target, block.id)) node->target = value.label; } m_gotos.push_back(node); }; const auto& term = block.terminator; if (term.kind == TerminatorKind::ConditionalBranch) { // Capture at the semantic block, before movement can pass an SCC/EXEC write. const auto condition = Expression(ExprOp::Variable, CaptureVariable(block.id)); add_goto(condition, term.true_block); add_goto(1, term.false_block); } else if (term.kind == TerminatorKind::Branch) { add_goto(1, term.true_block); } else if (term.kind == TerminatorKind::Return) { Insert(m_root, end, Kind::Return); } else { Fail("indirect control flow requires dispatcher lowering"); } for (const auto& value: continues) { if (value.after != block.id) continue; value.label->parent = m_root; value.label->position = m_root->children.insert(end, value.label); auto* repeat = Insert(m_root, end, Kind::Goto); repeat->target = labels[value.header]; m_gotos.push_back(repeat); } } } Graph Run() { for (auto it = m_gotos.rbegin(); it != m_gotos.rend() && m_failure == nullptr; ++it) { Eliminate(*it); } if (m_failure != nullptr) return Failed(); const auto entry = Allocate(); m_graph.entry_block = entry; Place(entry); Branch(Visit(m_root, entry, UINT32_MAX), UINT32_MAX); if (m_failure != nullptr) return Failed(); std::vector ordered; ordered.reserve(m_order.size()); for (const auto id: m_order) { ordered.push_back(std::move(m_graph.blocks[id])); } ApplyBlockOrder(m_graph, std::move(ordered)); RebuildPredecessors(m_graph); if (std::ranges::any_of(m_graph.blocks, [&](const auto& block) { return block.id != m_graph.entry_block && block.predecessors.empty(); })) { Fail("structured control flow requires an unreachable merge"); return Failed(); } RecomputeAnalyses(m_graph); if (m_graph.irreducible) { Fail("goto elimination produced irreducible control flow"); return Failed(); } return std::move(m_graph); } private: void Fail(const char* reason) { m_failure = reason; } Graph Failed() { SetFailure(m_graph, FailureKind::StructuredControlFlow, UINT32_MAX, m_failure); return std::move(m_graph); } uint32_t CaptureVariable(uint32_t block) const { return static_cast(m_route_expressions.size()) + block; } Node* New(Kind kind) { m_nodes.emplace_back(); m_nodes.back().kind = kind; return &m_nodes.back(); } Node* Insert(Node* parent, List::iterator before, Kind kind) { auto* node = New(kind); node->parent = parent; node->position = parent->children.insert(before, node); return node; } void Erase(Node* node) { node->parent->children.erase(node->position); } uint32_t Expression(ExprOp op, uint32_t lhs, uint32_t rhs = UINT32_MAX) { if (op == ExprOp::Not) { const auto& inner = m_graph.expressions[lhs]; if (inner.op == ExprOp::Constant) return inner.lhs ? 0 : 1; if (inner.op == ExprOp::Not) return inner.lhs; } if (op == ExprOp::Or) { if (lhs == 1 || rhs == 1) return 1; if (lhs == 0 || lhs == rhs) return rhs; if (rhs == 0) return lhs; } const auto result = static_cast(m_graph.expressions.size()); m_graph.expressions.push_back({op, lhs, rhs}); return result; } uint32_t RouteVariable(Node* label) { auto& expression = m_route_expressions[label->id]; if (expression == UINT32_MAX) { expression = Expression(ExprOp::Variable, label->id); // SSA initializes route variables to false; clear them again on each label visit. auto* reset = Insert(label->parent, std::next(label->position), Kind::Set); reset->id = label->id; reset->condition = 0; } return expression; } static size_t Depth(Node* node) { size_t depth = 0; for (; node->parent != nullptr; node = node->parent) ++depth; return depth; } static bool Related(Node* a, Node* b) { auto a_depth = Depth(a); auto b_depth = Depth(b); while (a_depth > b_depth) { a = a->parent; --a_depth; } while (b_depth > a_depth) { b = b->parent; --b_depth; } return a->parent == b->parent; } static bool Ordered(Node* a, Node* b) { for (auto it = a->position; it != a->parent->children.end(); ++it) { if (*it == b) return true; } return false; } static Node* Sibling(Node* uncle, Node* nephew) { while (nephew->parent != uncle->parent) nephew = nephew->parent; return nephew; } static void Adopt(Node* parent) { for (auto* child: parent->children) child->parent = parent; } void SetBefore(Node* node, uint32_t id, uint32_t expression) { auto* set = Insert(node->parent, node->position, Kind::Set); set->id = id; set->condition = expression; } Node* GotoAfter(Node* construct, Node* label, uint32_t condition) { auto* node = Insert(construct->parent, std::next(construct->position), Kind::Goto); node->target = label; node->condition = condition; return node; } Node* Outward(Node* node) { auto* parent = node->parent; auto* target = node->target; const auto variable = RouteVariable(target); SetBefore(node, target->id, node->condition); if (parent->kind == Kind::If) { auto* rest = Insert(parent, node->position, Kind::If); rest->condition = Expression(ExprOp::Not, variable); rest->children.splice(rest->children.end(), parent->children, std::next(node->position), parent->children.end()); Adopt(rest); } else if (parent->kind == Kind::Loop) { auto* stop = Insert(parent, node->position, Kind::Break); stop->condition = variable; } else { Fail("goto has no enclosing selection or loop"); return node; } Erase(node); return GotoAfter(parent, target, variable); } Node* Inward(Node* node) { auto* parent = node->parent; auto* target = node->target; auto* nested = Sibling(node, target); const auto variable = RouteVariable(target); SetBefore(node, target->id, node->condition); auto* rest = Insert(parent, node->position, Kind::If); rest->condition = Expression(ExprOp::Not, variable); rest->children.splice(rest->children.end(), parent->children, std::next(node->position), nested->position); Adopt(rest); Erase(node); if (nested->kind == Kind::If) { nested->condition = Expression(ExprOp::Or, variable, nested->condition); } else if (nested->kind != Kind::Loop) { Fail("goto target has no enclosing construct"); return node; } auto* moved = Insert(nested, nested->children.begin(), Kind::Goto); moved->target = target; moved->condition = variable; return moved; } Node* Lift(Node* node) { auto* parent = node->parent; auto* target = node->target; auto* nested = Sibling(node, target); const auto variable = RouteVariable(target); auto* loop = Insert(parent, node->position, Kind::Loop); loop->condition = variable; loop->children.splice(loop->children.end(), parent->children, nested->position, loop->position); Adopt(loop); if (std::ranges::any_of(loop->children, [](auto* child) { return child->kind == Kind::Break; })) { Fail("goto lifting would capture an enclosing loop exit"); return node; } auto* moved = Insert(loop, loop->children.begin(), Kind::Goto); moved->target = target; moved->condition = variable; auto* set = Insert(loop, loop->children.end(), Kind::Set); set->id = target->id; set->condition = node->condition; Erase(node); return moved; } void Eliminate(Node* node) { auto* target = node->target; while (node->parent != target->parent && m_failure == nullptr) { if (!Related(node, target) || Depth(node) > Depth(target)) { node = Outward(node); } else if (Ordered(Sibling(node, target), node)) { node = Lift(node); } else { node = Inward(node); } } if (m_failure != nullptr) return; if (std::next(node->position) == target->position) { Erase(node); return; } auto* parent = node->parent; if (Ordered(node, target)) { auto* selection = Insert(parent, node->position, Kind::If); selection->condition = Expression(ExprOp::Not, node->condition); selection->children.splice(selection->children.end(), parent->children, std::next(node->position), target->position); Adopt(selection); } else { auto* loop = Insert(parent, node->position, Kind::Loop); loop->condition = node->condition; loop->children.splice(loop->children.end(), parent->children, target->position, loop->position); Adopt(loop); } Erase(node); } uint32_t Allocate() { BasicBlock block; block.id = static_cast(m_graph.blocks.size()); m_graph.blocks.push_back(std::move(block)); return m_graph.blocks.back().id; } void Place(uint32_t block) { m_order.push_back(block); } void Branch(uint32_t from, uint32_t to) { if (from == UINT32_MAX) return; auto& block = m_graph.blocks[from]; block.terminator.kind = to == UINT32_MAX ? TerminatorKind::Return : TerminatorKind::Branch; block.terminator.true_block = to; block.successors = to == UINT32_MAX ? std::vector {} : std::vector {to}; } void Conditional(uint32_t block_id, uint32_t condition, uint32_t yes, uint32_t no, uint32_t merge = UINT32_MAX) { auto& block = m_graph.blocks[block_id]; auto& term = block.terminator; term.kind = TerminatorKind::ConditionalBranch; term.expression = condition; term.condition = BranchCondition::Expression; term.true_block = yes; term.false_block = no; term.merge_block = merge; block.successors = {yes, no}; } uint32_t Advance(uint32_t current) { const auto next = Allocate(); Branch(current, next); Place(next); return next; } bool RefersTo(uint32_t expression, uint32_t variable) const { const auto& value = m_graph.expressions[expression]; switch (value.op) { case ExprOp::Variable: return value.lhs == variable; case ExprOp::Not: return RefersTo(value.lhs, variable); case ExprOp::Or: return RefersTo(value.lhs, variable) || RefersTo(value.rhs, variable); default: return false; } } bool Modifies(Node* node, uint32_t expression) const { if (node->kind == Kind::Set && RefersTo(expression, node->id)) return true; if (node->kind == Kind::Code && m_source.blocks[node->id].terminator.kind == TerminatorKind::ConditionalBranch && RefersTo(expression, CaptureVariable(node->id))) return true; return std::ranges::any_of(node->children, [&](auto* child) { return Modifies(child, expression); }); } bool Complements(uint32_t first, uint32_t second) const { const auto& a = m_graph.expressions[first]; const auto& b = m_graph.expressions[second]; return (a.op == ExprOp::Not && a.lhs == second) || (b.op == ExprOp::Not && b.lhs == first); } uint32_t Visit(Node* parent, uint32_t current, uint32_t break_block) { for (auto it = parent->children.begin(); it != parent->children.end(); ++it) { auto* node = *it; if (node->kind == Kind::Label) continue; if (current == UINT32_MAX) { current = Allocate(); Place(current); } switch (node->kind) { case Kind::Code: { const auto& source = m_source.blocks[node->id]; const auto& previous = m_graph.blocks[current]; // Route assignments cannot observe guest writes; native predicates must stay at their original Code. const bool captures_native = std::ranges::any_of(previous.assignments, [&](const auto& assignment) { return m_graph.expressions[assignment.expression].op == ExprOp::Native; }); if (captures_native || (previous.inst_begin != previous.inst_end && previous.inst_end != source.inst_begin)) { current = Advance(current); } auto& block = m_graph.blocks[current]; if (block.inst_begin == block.inst_end) { block.start_pc = source.start_pc; block.inst_begin = source.inst_begin; } block.end_pc = source.end_pc; block.inst_end = source.inst_end; if (source.terminator.kind == TerminatorKind::ConditionalBranch) { block.assignments.push_back({CaptureVariable(source.id), Expression(ExprOp::Native, static_cast(source.terminator.condition))}); } break; } case Kind::Set: { m_graph.blocks[current].assignments.push_back({node->id, node->condition}); break; } case Kind::If: { if (node->condition == 0 || node->children.empty()) break; if (node->condition == 1) { current = Visit(node, current, break_block); break; } auto next = std::next(it); while (next != parent->children.end() && (*next)->kind == Kind::Label) ++next; Node* other = next != parent->children.end() ? *next : nullptr; const bool paired = other != nullptr && other->kind == Kind::If && Complements(node->condition, other->condition) && !Modifies(node, node->condition); const auto body = Allocate(); const auto merge = Allocate(); const auto alternative = paired ? Allocate() : merge; Conditional(current, node->condition, body, alternative, merge); Place(body); Branch(Visit(node, body, break_block), merge); if (paired) { Place(alternative); Branch(Visit(other, alternative, break_block), merge); it = next; } Place(merge); current = merge; break; } case Kind::Loop: { const auto& previous = m_graph.blocks[current]; const bool empty = previous.inst_begin == previous.inst_end && previous.assignments.empty(); const auto header = empty ? current : Advance(current); const auto body = Allocate(); const auto merge = Allocate(); Branch(header, body); Place(body); auto repeat = Visit(node, body, merge); // A construct merge belongs to the loop body, not its continue construct. if (repeat == UINT32_MAX || std::ranges::any_of(m_graph.blocks, [&](const auto& block) { return block.terminator.merge_block == repeat; })) { repeat = Advance(repeat); } auto& term = m_graph.blocks[header].terminator; term.loop_header = true; term.merge_block = merge; term.continue_block = repeat; if (node->condition <= 1) Branch(repeat, node->condition ? header : merge); else Conditional(repeat, node->condition, header, merge); Place(merge); current = merge; break; } case Kind::Break: { if (break_block == UINT32_MAX) { Fail("break outside a loop"); return UINT32_MAX; } if (node->condition == 0) break; if (node->condition == 1) { Branch(current, break_block); current = UINT32_MAX; } else { const auto next = Allocate(); Conditional(current, node->condition, break_block, next); Place(next); current = next; } break; } case Kind::Return: Branch(current, UINT32_MAX); current = UINT32_MAX; break; default: Fail("uneliminated control-flow statement"); return UINT32_MAX; } if (m_failure != nullptr) return UINT32_MAX; } return current; } const char* m_failure = nullptr; const Graph& m_source; Graph m_graph; std::deque m_nodes; Node* m_root = nullptr; std::vector m_gotos; std::vector m_route_expressions; std::vector m_order; }; } // namespace Graph BuildGraph(const Decoder::Program& program) { Graph graph; if (program.instructions.empty()) { ExitBuildFailure(graph, FailureKind::InvalidLabel, UINT32_MAX, "cannot build CFG for empty shader"); } const auto first_pc = program.instructions.front().pc; const auto end_pc = ProgramEndPc(program); for (const auto& inst: program.instructions) { if (inst.opcode == Opcode::UNSUPPORTED) { ExitBuildFailure( graph, FailureKind::UnsupportedInstruction, UINT32_MAX, fmt::format("unsupported decoded instruction in CFG at pc 0x{:08x}: {}", inst.pc, Decoder::InstructionToString(inst).c_str())); } } std::vector labels {first_pc, end_pc}; std::map setpc_targets; for (uint32_t i = 0; i < program.instructions.size(); i++) { const auto& inst = program.instructions[i]; const auto next_pc = InstructionEndPc(inst); if (Decoder::IsDirectBranch(inst.opcode) || (inst.opcode == Opcode::S_SWAPPC_B64 && inst.branch_target != UINT32_MAX)) { if (!IsValidTarget(inst.branch_target, program.instructions, end_pc)) { ExitBuildFailure(graph, FailureKind::InvalidBranchTarget, UINT32_MAX, fmt::format("branch at pc 0x{:08x} targets invalid pc 0x{:08x}", inst.pc, inst.branch_target)); } labels.push_back(inst.branch_target); if (next_pc <= end_pc) { labels.push_back(next_pc); } } else if (inst.opcode == Opcode::S_SETPC_B64) { SetpcTargetInfo target_info; if (!ResolveSetpcTargets(program, i, target_info)) { ExitBuildFailure( graph, FailureKind::InvalidBranchTarget, UINT32_MAX, fmt::format("unsupported dynamic S_SETPC_B64 at pc 0x{:08x}", inst.pc)); } const auto target_pcs = target_info.indirect ? std::span(target_info.target_pcs) : std::span(&target_info.target, 1); for (const auto target: target_pcs) { if (!IsValidTarget(target, program.instructions, end_pc)) { ExitBuildFailure( graph, FailureKind::InvalidBranchTarget, UINT32_MAX, fmt::format("S_SETPC_B64 at pc 0x{:08x} targets invalid pc 0x{:08x}", inst.pc, target)); } labels.push_back(target); } setpc_targets.emplace(inst.pc, std::move(target_info)); if (next_pc <= end_pc) { labels.push_back(next_pc); } } else if (inst.opcode == Opcode::S_ENDPGM || inst.opcode == Opcode::S_SWAPPC_B64) { labels.push_back(next_pc); } } SortUnique(labels); graph.blocks.reserve(labels.size()); for (uint32_t i = 0; i < labels.size(); i++) { const auto start = labels[i]; if (start > end_pc) { continue; } const auto begin = std::ranges::lower_bound(program.instructions, start, {}, &Instruction::pc); if (start != end_pc && (begin == program.instructions.end() || begin->pc != start)) { ExitBuildFailure( graph, FailureKind::InvalidLabel, UINT32_MAX, fmt::format("CFG label does not start on an instruction: 0x{:08x}", start)); } BasicBlock block; block.id = static_cast(graph.blocks.size()); block.start_pc = start; block.end_pc = i + 1u < labels.size() ? labels[i + 1u] : end_pc; block.inst_begin = static_cast(begin - program.instructions.begin()); block.inst_end = static_cast( std::lower_bound(program.instructions.begin(), program.instructions.end(), block.end_pc, [](const Instruction& inst, uint32_t pc) { return inst.pc < pc; }) - program.instructions.begin()); graph.blocks.push_back(std::move(block)); } graph.entry_block = 0; const auto block_at_pc = [&](uint32_t pc) { const auto found = std::ranges::lower_bound(graph.blocks, pc, {}, &BasicBlock::start_pc); return found != graph.blocks.end() && found->start_pc == pc ? found->id : UINT32_MAX; }; for (auto& block: graph.blocks) { block.terminator = {}; if (block.inst_begin == block.inst_end) { block.terminator.kind = TerminatorKind::Return; continue; } const auto& last = program.instructions[block.inst_end - 1u]; const auto next_pc = InstructionEndPc(last); if (last.opcode == Opcode::S_ENDPGM) { block.terminator.kind = TerminatorKind::Return; } else if (last.opcode == Opcode::S_SWAPPC_B64 && last.branch_target != UINT32_MAX) { block.terminator.kind = TerminatorKind::Branch; block.terminator.true_block = block_at_pc(last.branch_target); } else if (last.opcode == Opcode::S_SETPC_B64) { const auto& target_info = setpc_targets.at(last.pc); if (target_info.indirect) { block.terminator.kind = TerminatorKind::IndirectBranch; block.terminator.condition = BranchCondition::Always; block.terminator.indirect_pc_sgpr = target_info.pc_sgpr; block.terminator.indirect_selector_code = target_info.selector_code; for (const auto target_pc: target_info.target_pcs) { block.terminator.indirect_target_pcs.push_back(target_pc); block.terminator.indirect_targets.push_back(block_at_pc(target_pc)); } const auto selector_count = std::min(target_info.selector_values.size(), target_info.selector_target_pcs.size()); for (uint32_t i = 0; i < selector_count; i++) { block.terminator.indirect_selector_values.push_back( target_info.selector_values[i]); block.terminator.indirect_selector_targets.push_back( block_at_pc(target_info.selector_target_pcs[i])); } } else { block.terminator.kind = TerminatorKind::Branch; block.terminator.condition = BranchCondition::Always; block.terminator.true_block = block_at_pc(target_info.target); } } else if (last.opcode == Opcode::S_BRANCH) { block.terminator.kind = TerminatorKind::Branch; block.terminator.condition = BranchCondition::Always; block.terminator.true_block = block_at_pc(last.branch_target); } else if (Decoder::IsConditionalBranch(last.opcode)) { block.terminator.kind = TerminatorKind::ConditionalBranch; block.terminator.condition = ConditionForOpcode(last.opcode); block.terminator.true_block = block_at_pc(last.branch_target); const auto fallthrough = block_at_pc(next_pc); if (fallthrough == UINT32_MAX) { ExitBuildFailure( graph, FailureKind::MissingFallthrough, block.id, fmt::format("conditional branch at pc 0x{:08x} has no fallthrough block", last.pc)); } block.terminator.false_block = fallthrough; } else { const auto next = block_at_pc(block.end_pc); if (next != UINT32_MAX && block.end_pc != block.start_pc) { block.terminator.kind = TerminatorKind::Branch; block.terminator.condition = BranchCondition::Always; block.terminator.true_block = next; } else { block.terminator.kind = TerminatorKind::Return; } } switch (block.terminator.kind) { case TerminatorKind::Branch: AddUnique(block.successors, block.terminator.true_block); break; case TerminatorKind::ConditionalBranch: AddUnique(block.successors, block.terminator.true_block); AddUnique(block.successors, block.terminator.false_block); break; case TerminatorKind::IndirectBranch: for (const auto target: block.terminator.indirect_targets) { AddUnique(block.successors, target); } break; case TerminatorKind::Return: case TerminatorKind::Unsupported: break; } } for (auto& block: graph.blocks) { SortUnique(block.successors); for (auto succ: block.successors) { AddUnique(graph.blocks[succ].predecessors, block.id); } } for (auto& block: graph.blocks) { SortUnique(block.predecessors); } PruneUnreachableBlocks(graph); graph.code_table_load_pcs.clear(); bool indirect_setpc = false; for (const auto& block: graph.blocks) { if (block.terminator.kind != TerminatorKind::IndirectBranch) { continue; } indirect_setpc = true; if (block.inst_begin >= block.inst_end || block.inst_end > program.instructions.size()) { continue; } const auto found = setpc_targets.find(program.instructions[block.inst_end - 1u].pc); if (found != setpc_targets.end() && found->second.table_load_pc != UINT32_MAX) { AddUnique(graph.code_table_load_pcs, found->second.table_load_pc); } } SortUnique(graph.code_table_load_pcs); RecomputeAnalyses(graph); if (indirect_setpc) { graph.irreducible = true; graph.unsupported = false; graph.failure_kind = FailureKind::IrreducibleControlFlow; const auto indirect = std::find_if(graph.blocks.begin(), graph.blocks.end(), [](const auto& block) { return block.terminator.kind == TerminatorKind::IndirectBranch; }); graph.failure_block = indirect != graph.blocks.end() ? indirect->id : graph.entry_block; graph.unsupported_reason = "indirect S_SETPC_B64 jump table requires dispatcher fallback"; } if (graph.irreducible) { graph.unsupported = false; } return graph; } Graph Structurize(const Graph& graph) { if (graph.unsupported || graph.irreducible) { Graph failed; SetFailure(failed, graph.failure_kind, graph.failure_block, graph.unsupported_reason); return failed; } return GotoStructurizer(graph).Run(); } bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code) { uint32_t destination = UINT32_MAX; // Scalar ALU pair widths are not all decoded. The typed producer must confirm this word. return InstructionWritesScalarCode(inst, code) || (ScalarOperandCode(inst.dst, destination) && code == destination + 1u); } uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& graph, uint32_t before, uint32_t code) { EXIT_IF(before >= program.instructions.size()); const auto found = std::ranges::find_if(graph.blocks, [before](const auto& block) { return block.inst_begin <= before && before < block.inst_end; }); EXIT_IF(found == graph.blocks.end()); const auto* block = &*found; for (size_t depth = 0; depth < graph.blocks.size(); ++depth) { for (uint32_t i = before; i > block->inst_begin;) { const auto& inst = program.instructions[--i]; EXIT_IF(inst.opcode == Opcode::S_SWAPPC_B64); if (MayWriteScalarRegister(inst, code)) return i; } if (block->id == graph.entry_block) return UINT32_MAX; EXIT_IF(block->predecessors.size() != 1u); block = graph.FindBlock(block->predecessors[0]); EXIT_IF(block == nullptr); before = block->inst_end; } EXIT("scalar shader call source has cyclic reaching definitions"); } std::string BranchConditionToString(BranchCondition condition) { switch (condition) { case BranchCondition::Always: return "always"; case BranchCondition::SccZero: return "scc0"; case BranchCondition::SccNonZero: return "scc1"; case BranchCondition::VccZero: return "vccz"; case BranchCondition::VccNonZero: return "vccnz"; case BranchCondition::ExecZero: return "execz"; case BranchCondition::ExecNonZero: return "execnz"; case BranchCondition::ScalarInstruction: return "scalar_instruction"; case BranchCondition::Expression: return "expression"; default: return "unknown"; } } std::string FailureKindToString(FailureKind kind) { switch (kind) { case FailureKind::None: return "None"; case FailureKind::InvalidInput: return "InvalidInput"; case FailureKind::UnsupportedInstruction: return "UnsupportedInstruction"; case FailureKind::InvalidBranchTarget: return "InvalidBranchTarget"; case FailureKind::MissingFallthrough: return "MissingFallthrough"; case FailureKind::InvalidLabel: return "InvalidLabel"; case FailureKind::IrreducibleControlFlow: return "IrreducibleControlFlow"; case FailureKind::StructuredControlFlow: return "StructuredControlFlow"; default: return "Unknown"; } } std::string GraphToString(const Graph& graph) { std::string text; text += fmt::format("entry_block={} irreducible={} unsupported={} failure={} failure_block={}\n", graph.entry_block, graph.irreducible ? 1u : 0u, graph.unsupported ? 1u : 0u, FailureKindToString(graph.failure_kind).c_str(), graph.failure_block); if (!graph.unsupported_reason.empty()) { text += "unsupported_reason="; text += graph.unsupported_reason; text += "\n"; } for (const auto& block: graph.blocks) { text += fmt::format("block_{} pc=0x{:08x} end=0x{:08x} inst=[{},{})\n", block.id, block.start_pc, block.end_pc, block.inst_begin, block.inst_end); text += fmt::format(" predecessors=[{}] successors=[{}]\n", VectorToString(block.predecessors).c_str(), VectorToString(block.successors).c_str()); text += fmt::format(" dominators=[{}]\n", VectorToString(block.dominators).c_str()); text += fmt::format( " terminator={} condition={} true={} false={} merge={} continue={} loop_header={} " "indirect_sgpr={} indirect_selector={} indirect_targets=[{}] selector_values=[{}] " "expression={}\n", static_cast(block.terminator.kind), BranchConditionToString(block.terminator.condition).c_str(), block.terminator.true_block, block.terminator.false_block, block.terminator.merge_block, block.terminator.continue_block, block.terminator.loop_header ? 1u : 0u, block.terminator.indirect_pc_sgpr, block.terminator.indirect_selector_code, VectorToString(block.terminator.indirect_targets).c_str(), VectorToString(block.terminator.indirect_selector_values).c_str(), block.terminator.expression); for (const auto& assignment: block.assignments) { text += fmt::format(" variable_{} = expression_{}", assignment.variable, assignment.expression); const auto& expression = graph.expressions[assignment.expression]; if (expression.op == ConditionExpression::Op::Native) text += fmt::format(" condition={}", BranchConditionToString(static_cast(expression.lhs))); text += "\n"; } } for (const auto& edge: graph.back_edges) { text += fmt::format("backedge {} -> {} natural={}\n", edge.from, edge.to, edge.natural ? 1u : 0u); } for (const auto& loop: graph.natural_loops) { text += fmt::format("loop header={} latch={} body=[{}]\n", loop.header, loop.latch, VectorToString(loop.body_blocks).c_str()); } for (const auto& component: graph.components) { text += fmt::format("scc blocks=[{}] entries=[{}] irreducible={}\n", VectorToString(component.blocks).c_str(), VectorToString(component.entry_blocks).c_str(), component.irreducible ? 1u : 0u); } return text; } } // namespace Libs::Graphics::ShaderRecompiler::CFG