#include "stdafx.h" #include "SPUDisAsm.h" #include "SPUAnalyser.h" #include "SPUThread.h" const spu_decoder s_spu_disasm; const extern spu_decoder g_spu_itype; const extern spu_decoder g_spu_iname; const extern spu_decoder g_spu_iflag; #include "util/v128.hpp" #include "util/simd.hpp" u32 SPUDisAsm::disasm(u32 pc) { last_opcode.clear(); if (pc < m_start_pc || pc >= SPU_LS_SIZE) { return 0; } dump_pc = pc; be_t op; std::memcpy(&op, m_offset + pc, 4); m_op = op; (this->*(s_spu_disasm.decode(m_op)))({ m_op }); format_by_mode(); return 4; } std::pair SPUDisAsm::get_memory_span() const { return {m_offset + m_start_pc, SPU_LS_SIZE - m_start_pc}; } std::unique_ptr SPUDisAsm::copy_type_erased() const { return std::make_unique(*this); } std::pair SPUDisAsm::try_get_const_value(u32 reg, u32 pc, u32 TTL) const { if (!TTL) { // Recursion limit (Time To Live) return {}; } if (pc == umax) { // Default arg: choose pc of previous instruction if (dump_pc == 0) { // Do not underflow return {}; } pc = dump_pc - 4; } // Scan LS backwards from this instruction (until PC=0) // Search for the first register modification or branch instruction for (s32 i = static_cast(pc); i >= static_cast(m_start_pc); i -= 4) { const u32 opcode = *reinterpret_cast*>(m_offset + i); const spu_opcode_t op0{ opcode }; const auto type = g_spu_itype.decode(opcode); if (type & spu_itype::branch || type == spu_itype::UNK || !opcode) { if (reg < 80u) { return {}; } // We do not care about function calls if register is non-volatile if ((type != spu_itype::BRSL && type != spu_itype::BRASL && type != spu_itype::BISL) || op0.rt == reg) { return {}; } continue; } // Get constant register value #define GET_CONST_REG(var, reg) \ {\ /* Search for the constant value of the register*/\ const auto [is_const, value] = try_get_const_value(reg, i - 4, TTL - 1);\ \ if (!is_const)\ {\ /* Cannot compute constant value if register is not constant*/\ return {};\ }\ \ var = value;\ } void() /*<- Require a semicolon*/ //const auto flag = g_spu_iflag.decode(opcode); // TODO: It detects spurious register modifications if (u32 dst = type & spu_itype::_quadrop ? +op0.rt4 : +op0.rt; dst == reg) { // Note: It's not 100% reliable because it won't detect branch targets within [i, dump_pc] range (e.g. if-else statement for command's value) switch (type) { case spu_itype::IL: { return { true, v128::from32p(op0.si16) }; } case spu_itype::ILA: { return { true, v128::from32p(op0.i18) }; } case spu_itype::ILHU: { return { true, v128::from32p(op0.i16 << 16) }; } case spu_itype::ILH: { return { true, v128::from16p(op0.i16) }; } case spu_itype::CBD: case spu_itype::CHD: case spu_itype::CWD: case spu_itype::CDD: { // Aligned stack assumption if (op0.ra == 1u) { u32 size = 0; switch (type) { case spu_itype::CBD: size = 1; break; case spu_itype::CHD: size = 2; break; case spu_itype::CWD: size = 4; break; case spu_itype::CDD: size = 8; break; default: fmt::throw_exception("Unreachable"); } const u32 index = (~op0.i7 & 0xf) / size; auto res = v128::from64(0x18191A1B1C1D1E1Full, 0x1011121314151617ull); switch (size) { case 1: res._u8[index] = 0x03; break; case 2: res._u16[index] = 0x0203; break; case 4: res._u32[index] = 0x00010203; break; case 8: res._u64[index] = 0x0001020304050607ull; break; default: fmt::throw_exception("Unreachable"); } return {true, res}; } return {}; } case spu_itype::FSMBI: { v128 res; for (s32 i = 0; i < 16; i++) { res._u8[i] = (op0.i16 & (1 << i)) ? 0xFF : 0x00; } return { true, res }; } case spu_itype::IOHL: { v128 reg_val{}; // Search for ILHU+IOHL pattern (common pattern for 32-bit constants formation) // But don't limit to it GET_CONST_REG(reg_val, op0.rt); return { true, reg_val | v128::from32p(op0.i16) }; } case spu_itype::STQA: case spu_itype::STQD: case spu_itype::STQR: case spu_itype::STQX: case spu_itype::WRCH: { // Do not modify RT break; } case spu_itype::SHLQBYI: { if (op0.si7) { // Unimplemented, doubt needed return {}; } // Move register value operation v128 reg_val{}; GET_CONST_REG(reg_val, op0.ra); return { true, reg_val }; } case spu_itype::ORI: { v128 reg_val{}; GET_CONST_REG(reg_val, op0.ra); return { true, reg_val | v128::from32p(op0.si10) }; } default: return {}; } } } return {}; } SPUDisAsm::insert_mask_info SPUDisAsm::try_get_insert_mask_info(const v128& mask) { if ((mask & v128::from8p(0xe0)) != v128{}) { return {}; } s32 first = -1, src_first = 0, last = 16; auto access = [&](u32 index) -> u8 { return mask._u8[index ^ 0xf]; }; for (s32 i = 0; i < 16; i++) { if (access(i) & 0x10) { if ((access(i) & 0x0f) != i) { return {}; } if (first != -1 && last == 16) { last = i; } continue; } if (last != 16) { return {}; } if (first == -1) { src_first = access(i); first = i; } if (src_first + (i - first) != access(i)) { return {}; } } if (first == -1) { return {}; } const u32 size = last - first; if ((size | src_first | first) & (size - 1)) { return {}; } if (size == 16) { // 0x0, 0x1, 0x2, .. 0xE, 0xF is not allowed return {}; } // [type size, dst index, src index] return {size, first / size, src_first / size}; } void SPUDisAsm::WRCH(spu_opcode_t op) { DisAsm("wrch", spu_ch_name[op.ra], spu_reg_name[op.rt]); const auto [is_const, value] = try_get_const_value(op.rt); if (is_const) { switch (op.ra) { case MFC_Cmd: { fmt::append(last_opcode, " #%s", MFC(value._u8[12])); return; } case MFC_WrListStallAck: case MFC_WrTagMask: { const u32 v = value._u32[3]; if (v && !(v & (v - 1))) fmt::append(last_opcode, " #%s (tag=%u)", SignedHex(v), std::countr_zero(v)); // Single-tag mask else fmt::append(last_opcode, " #%s", SignedHex(v)); // Multi-tag mask (or zero) return; } case MFC_EAH: { fmt::append(last_opcode, " #%s", SignedHex(value._u32[3])); return; } case MFC_Size: { fmt::append(last_opcode, " #%s", SignedHex(value._u16[6])); return; } case MFC_TagID: { fmt::append(last_opcode, " #%u", value._u8[12]); return; } case MFC_WrTagUpdate: { const auto upd = fmt::format("%s", mfc_tag_update(value._u32[3])); fmt::append(last_opcode, " #%s", upd == "empty" ? "IMMEDIATE" : upd); return; } case SPU_WrOutIntrMbox: { const u32 code = value._u32[3] >> 24; if (code == 128u) { last_opcode += " #sys_event_flag_set_bit"; } else if (code == 192u) { last_opcode += " #sys_event_flag_set_bit_impatient"; } else { fmt::append(last_opcode, " #%s", SignedHex(value._u32[3])); } return; } default: { fmt::append(last_opcode, " #%s", SignedHex(value._u32[3])); return; } } } } enum CellError : u32; void SPUDisAsm::IOHL(spu_opcode_t op) { DisAsm("iohl", spu_reg_name[op.rt], op.i16); const auto [is_const, value] = try_get_const_equal_value_array(op.rt); // Only print constant for a 4 equal 32-bit constants array if (is_const) { comment_constant(last_opcode, value | op.i16); } } void SPUDisAsm::SHUFB(spu_opcode_t op) { DisAsm("shufb", spu_reg_name[op.rt4], spu_reg_name[op.ra], spu_reg_name[op.rb], spu_reg_name[op.rc]); const auto [is_const, value] = try_get_const_value(op.rc); if (is_const) { const auto [size, dst, src] = try_get_insert_mask_info(value); if (size) { if ((size >= 4u && !src) || (size == 2u && src == 1u) || (size == 1u && src == 3u)) { // Comment insertion pattern for CWD-alike instruction fmt::append(last_opcode, " #i%u[%u]", size * 8, dst); return; } // Comment insertion pattern for unknown instruction formations fmt::append(last_opcode, " #i%u[%u] = [%u]", size * 8, dst, src); return; } } }