diff --git a/Makefile.common b/Makefile.common index d7203ee3..cddb2238 100644 --- a/Makefile.common +++ b/Makefile.common @@ -14,6 +14,7 @@ SOURCES_CXX := \ $(CORE_DIR)/core/hw/arm7/arm7.cpp \ $(CORE_DIR)/core/hw/arm7/arm7_rec.cpp \ $(CORE_DIR)/core/hw/arm7/arm7_rec_x64.cpp \ + $(CORE_DIR)/core/hw/arm7/arm7_rec_x86.cpp \ $(CORE_DIR)/core/hw/arm7/arm7_rec_arm64.cpp \ $(CORE_DIR)/core/hw/arm7/vbaARM.cpp \ \ diff --git a/core/build.h b/core/build.h index 4be2dced..1c3c41fc 100644 --- a/core/build.h +++ b/core/build.h @@ -231,7 +231,7 @@ #endif #ifndef FEAT_AREC - #if HOST_CPU == CPU_ARM || HOST_CPU == CPU_ARM64 || HOST_CPU == CPU_X64 + #if HOST_CPU == CPU_ARM || HOST_CPU == CPU_ARM64 || HOST_CPU == CPU_X64 || HOST_CPU == CPU_X86 #define FEAT_AREC DYNAREC_JIT #else #define FEAT_AREC DYNAREC_NONE @@ -240,9 +240,10 @@ // Where the ARM7's recompiler can be switched for the interpreter while // running, which the build then also has: a core option does it. (The -// x86-64 and the ARM64 recompilers are one, arm7_rec.cpp, with a backend -// for each, and run a sound program the same; so does the interpreter.) -#if (HOST_CPU == CPU_X64 || HOST_CPU == CPU_ARM64) && FEAT_AREC != DYNAREC_NONE +// x86-64, 32-bit x86 and ARM64 recompilers are one, arm7_rec.cpp, with a +// backend for each, and run a sound program the same; so does the +// interpreter.) +#if (HOST_CPU == CPU_X64 || HOST_CPU == CPU_X86 || HOST_CPU == CPU_ARM64) && FEAT_AREC != DYNAREC_NONE #define FEAT_AREC_SELECTABLE 1 #endif diff --git a/core/hw/arm7/arm7.cpp b/core/hw/arm7/arm7.cpp index e3b81550..98305566 100644 --- a/core/hw/arm7/arm7.cpp +++ b/core/hw/arm7/arm7.cpp @@ -1,5 +1,5 @@ #include "arm7.h" -#if FEAT_AREC != DYNAREC_NONE && (HOST_CPU == CPU_X64 || HOST_CPU == CPU_ARM64) +#if FEAT_AREC != DYNAREC_NONE && (HOST_CPU == CPU_X64 || HOST_CPU == CPU_X86 || HOST_CPU == CPU_ARM64) // before the interpreter's macros, which take some of its names #include "arm7_rec.h" #endif @@ -368,10 +368,10 @@ void update_armintc() reg[INTR_PEND].I=e68k_out && armFiqEnable; } -#if FEAT_AREC != DYNAREC_NONE && (HOST_CPU == CPU_X64 || HOST_CPU == CPU_ARM64) +#if FEAT_AREC != DYNAREC_NONE && (HOST_CPU == CPU_X64 || HOST_CPU == CPU_X86 || HOST_CPU == CPU_ARM64) // -// For the recompiler in arm7_rec.cpp, with arm7_rec_x64.cpp or -// arm7_rec_arm64.cpp +// For the recompiler in arm7_rec.cpp, with arm7_rec_x64.cpp, +// arm7_rec_x86.cpp or arm7_rec_arm64.cpp // namespace aicaarm { @@ -459,8 +459,8 @@ void armt_init() #elif FEAT_AREC != DYNAREC_NONE -/* The recompiler below is for 32-bit ARM: there is none for 32-bit x86, - * which runs the interpreter. */ +/* The recompiler below is for 32-bit ARM only: the other processors have + * arm7_rec.cpp's. */ #if HOST_CPU != CPU_ARM #error "no ARM7 recompiler for this processor: build it with FEAT_AREC=DYNAREC_NONE" #endif diff --git a/core/hw/arm7/arm7_rec.cpp b/core/hw/arm7/arm7_rec.cpp index 388047a3..844f169e 100644 --- a/core/hw/arm7/arm7_rec.cpp +++ b/core/hw/arm7/arm7_rec.cpp @@ -19,7 +19,7 @@ #include "build.h" -#if (HOST_CPU == CPU_X64 || HOST_CPU == CPU_ARM64) && FEAT_AREC != DYNAREC_NONE +#if (HOST_CPU == CPU_X64 || HOST_CPU == CPU_X86 || HOST_CPU == CPU_ARM64) && FEAT_AREC != DYNAREC_NONE #include "arm7_rec.h" #include "arm7.h" diff --git a/core/hw/arm7/arm7_rec_x86.cpp b/core/hw/arm7/arm7_rec_x86.cpp new file mode 100644 index 00000000..481ddba3 --- /dev/null +++ b/core/hw/arm7/arm7_rec_x86.cpp @@ -0,0 +1,940 @@ +/* + Copyright 2020 flyinghead + + This file is part of flycast. + + flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with flycast. If not, see . + */ + +/* The sound processor's recompiler for 32-bit x86, with xbyak. + * + * It is arm7_rec_x64.cpp made for a processor with eight registers, and + * does what that one does for every instruction, in the same order and + * with the same results, flags and memory accesses: the blocks, their + * cycles and the register allocation are arm7_rec.cpp's, shared by every + * backend. What differs is what the processor has: + * + * - four registers for the ARM's (ebx, ebp, esi, edi, which the C code + * called keeps), and eax, ecx and edx for everything else. The two + * operands of an instruction are taken into edx and eax, and the + * shifter's carry, where it is wanted, into ecx. Where x86-64 had a + * spare register for a value to use in place of a shift's, the value is + * in memory (cmov takes it from there), and a rotate through the carry + * (RRX) is the host's own rcr, with the ARM's carry put in the host's + * by bt; + * - the emulator's data reached at its address, which 32 bits can always + * give, where x86-64 reaches it relative to the instruction; + * - calls made fastcall, the first two arguments in ecx and edx, as every + * handler called from here is declared (DYNACALL), with the stack a + * multiple of 16 at each one, as Linux and Android want; + * - four bytes to an entry point. + * + * Everything blocks jump to and call that is not C - the main loop, the + * routine that has a block compiled - is written into the code buffer by + * arm7backend_flush(), so nothing here is assembly linked into the + * library, which keeps it free of relocations in its code (Android refuses + * a library that has them). */ + +#include "build.h" + +#if HOST_CPU == CPU_X86 && FEAT_AREC != DYNAREC_NONE + +#include "deps/xbyak/xbyak.h" +#include "deps/xbyak/xbyak_util.h" +using namespace Xbyak::util; + +#include "arm7_rec.h" +#include "hw/aica/aica_if.h" + +namespace aicaarm +{ + +static void (*arm_dispatch)(); + +// fastcall +static const Xbyak::Reg32 call_regs[] = { ecx, edx }; +static void (**entry_points)(); + +/* What a shift by a register gives in place of its own result for a count + * of 32 or more: 0 for the logical shifts, and the count an arithmetic + * shift by 31 is the same as. */ +static const u32 shift_zero = 0; +static const u32 shift_asr_max = 31; + +class Arm7Compiler; + +#define ALLOC_REGS 4 +static const std::array alloc_regs { + ebx, ebp, esi, edi +}; + +class X86ArmRegAlloc : public ArmRegAlloc +{ + using super = ArmRegAlloc; + Arm7Compiler& assembler; + + void LoadReg(int host_reg, Arm7Reg armreg); + void StoreReg(int host_reg, Arm7Reg armreg); + + static const Xbyak::Reg32& getReg32(int i) + { + verify(i >= 0 && (u32)i < alloc_regs.size()); + return alloc_regs[i]; + } + +public: + X86ArmRegAlloc(Arm7Compiler& assembler, const std::vector& block_ops) + : super(block_ops), assembler(assembler) {} + + const Xbyak::Reg32& map(Arm7Reg r) + { + int i = super::map(r); + return getReg32(i); + } + + friend super; +}; + +class Arm7Compiler : public Xbyak::CodeGenerator +{ + bool logical_op_set_flags = false; + bool set_carry_bit = false; + bool set_flags = false; + X86ArmRegAlloc *regalloc = nullptr; + + static const u32 N_FLAG = 1 << 31; + static const u32 Z_FLAG = 1 << 30; + static const u32 C_FLAG = 1 << 29; + static const u32 V_FLAG = 1 << 28; + + // the ARM's flags, in its register file + Xbyak::Address armFlags() const { + return dword[&arm_Reg[RN_PSR_FLAGS].I]; + } + + /* The shifter's carry goes in ecx (bit 0, the rest 0, or bit 0 of cl + * with the rest of ecx anything above bit 7: saveFlags() only keeps + * bit 0's place). */ + Xbyak::Operand getOperand(const ArmOp::Operand& arg, Xbyak::Reg32 scratch_reg) + { + Xbyak::Reg32 r; + if (!arg.isReg()) + { + /* A plain constant is left to the caller. One that still has + * a shift to go through is r15 read as a value, with a shift + * the decoder could not work out ahead. */ + if (arg.isNone() || !arg.isShifted()) + return Xbyak::Operand(); + mov(scratch_reg, arg.getImmediate()); + r = scratch_reg; + } + else + r = regalloc->map(arg.getReg().armreg); + if (arg.isShifted()) + { + if (r != scratch_reg) + { + mov(scratch_reg, r); + r = scratch_reg; + } + if (arg.shift_imm) + { + // shift by immediate + if (arg.shift_type != ArmOp::ROR && arg.shift_value != 0 && !logical_op_set_flags) + { + switch (arg.shift_type) + { + case ArmOp::LSL: + shl(r, arg.shift_value); + break; + case ArmOp::LSR: + shr(r, arg.shift_value); + break; + case ArmOp::ASR: + sar(r, arg.shift_value); + break; + default: + die("invalid"); + break; + } + } + else if (arg.shift_value == 0) + { + // Shift by 32 + if (logical_op_set_flags) + set_carry_bit = true; + if (arg.shift_type == ArmOp::LSR) + { + if (set_carry_bit) + { + mov(ecx, r); // ecx = rm[31] + shr(ecx, 31); + } + mov(r, 0); // r = 0 + } + else if (arg.shift_type == ArmOp::ASR) + { + if (set_carry_bit) + { + mov(ecx, r); // ecx = rm[31] + shr(ecx, 31); + } + sar(r, 31); // r = rm < 0 ? -1 : 0 + } + else if (arg.shift_type == ArmOp::ROR) + { + // RRX: the ARM's carry into the top, bit 0 out to the host's carry + bt(armFlags(), 29); + rcr(r, 1); + if (set_carry_bit) + setc(cl); // cl = rm[0] (new C) + } + else + die("Invalid shift"); + } + else + { + // Carry must be preserved or Ror shift + if (logical_op_set_flags) + set_carry_bit = true; + if (arg.shift_type == ArmOp::LSL) + { + if (set_carry_bit) + { + mov(ecx, r); + shr(ecx, 32 - arg.shift_value); + } + shl(r, arg.shift_value); // r <<= shift + if (set_carry_bit) + and_(ecx, 1); // ecx = rm[lsb] + } + else + { + if (set_carry_bit) + { + mov(ecx, r); + shr(ecx, arg.shift_value - 1); + and_(ecx, 1); // ecx = rm[msb] + } + + if (arg.shift_type == ArmOp::LSR) + shr(r, arg.shift_value); // r >>= shift + else if (arg.shift_type == ArmOp::ASR) + sar(r, arg.shift_value); + else if (arg.shift_type == ArmOp::ROR) + ror(r, arg.shift_value); + else + die("Invalid shift"); + } + } + } + else + { + // shift by register + const Xbyak::Reg32 shift_reg = regalloc->map(arg.shift_reg.armreg); + /* The count is the low byte of the register, whatever the + * rest of it holds. (esi, edi and ebp have no byte register + * of their own here: the whole register is taken first.) */ + switch (arg.shift_type) + { + case ArmOp::LSL: + case ArmOp::LSR: + mov(ecx, shift_reg); + movzx(ecx, cl); + if (arg.shift_type == ArmOp::LSL) + shl(r, cl); + else + shr(r, cl); + cmp(ecx, 32); + cmovnb(r, dword[&shift_zero]); // LSL and LSR by 32 or more gives 0 + break; + case ArmOp::ASR: + mov(ecx, shift_reg); + movzx(ecx, cl); + cmp(ecx, 32); + cmovnb(ecx, dword[&shift_asr_max]); // ASR by 32 or more gives 0 or -1 depending on operand sign, as by 31 + sar(r, cl); + break; + case ArmOp::ROR: + mov(ecx, shift_reg); + ror(r, cl); + break; + default: + die("Invalid shift"); + break; + } + } + } + return r; + } + + Xbyak::Label *startConditional(ArmOp::Condition cc) + { + if (cc == ArmOp::AL) + return nullptr; + Xbyak::Label *label = new Xbyak::Label(); + cc = (ArmOp::Condition)((u32)cc ^ 1); // invert the condition + mov(eax, armFlags()); + switch (cc) + { + case ArmOp::EQ: // Z==1 + and_(eax, Z_FLAG); + jnz(*label, T_NEAR); + break; + case ArmOp::NE: // Z==0 + and_(eax, Z_FLAG); + jz(*label, T_NEAR); + break; + case ArmOp::CS: // C==1 + and_(eax, C_FLAG); + jnz(*label, T_NEAR); + break; + case ArmOp::CC: // C==0 + and_(eax, C_FLAG); + jz(*label, T_NEAR); + break; + case ArmOp::MI: // N==1 + and_(eax, N_FLAG); + jnz(*label, T_NEAR); + break; + case ArmOp::PL: // N==0 + and_(eax, N_FLAG); + jz(*label, T_NEAR); + break; + case ArmOp::VS: // V==1 + and_(eax, V_FLAG); + jnz(*label, T_NEAR); + break; + case ArmOp::VC: // V==0 + and_(eax, V_FLAG); + jz(*label, T_NEAR); + break; + case ArmOp::HI: // (C==1) && (Z==0) + and_(eax, C_FLAG | Z_FLAG); + cmp(eax, C_FLAG); + jz(*label, T_NEAR); + break; + case ArmOp::LS: // (C==0) || (Z==1) + and_(eax, C_FLAG | Z_FLAG); + cmp(eax, C_FLAG); + jnz(*label, T_NEAR); + break; + case ArmOp::GE: // N==V + mov(ecx, eax); + shl(ecx, 3); + xor_(eax, ecx); + and_(eax, N_FLAG); + jz(*label, T_NEAR); + break; + case ArmOp::LT: // N!=V + mov(ecx, eax); + shl(ecx, 3); + xor_(eax, ecx); + and_(eax, N_FLAG); + jnz(*label, T_NEAR); + break; + case ArmOp::GT: // (Z==0) && (N==V) + mov(ecx, eax); + mov(edx, eax); + shl(ecx, 3); + shl(edx, 1); + xor_(eax, ecx); + or_(eax, edx); + and_(eax, N_FLAG); + jz(*label, T_NEAR); + break; + case ArmOp::LE: // (Z==1) || (N!=V) + mov(ecx, eax); + mov(edx, eax); + shl(ecx, 3); + shl(edx, 1); + xor_(eax, ecx); + or_(eax, edx); + and_(eax, N_FLAG); + jnz(*label, T_NEAR); + break; + default: + die("Invalid condition code"); + break; + } + + return label; + } + + void endConditional(Xbyak::Label *label) + { + if (label != nullptr) + { + L(*label); + delete label; + } + } + + bool emitDataProcOp(const ArmOp& op) + { + bool save_v_flag = true; + + Xbyak::Operand arg0 = getOperand(op.arg[0], edx); + Xbyak::Operand arg1 = getOperand(op.arg[1], eax); + Xbyak::Reg32 rd; + if (op.rd.isReg()) + rd = regalloc->map(op.rd.getReg().armreg); + if (logical_op_set_flags) + { + // When an Operand2 constant is used with the instructions MOVS, MVNS, ANDS, ORRS, ORNS, EORS, BICS, TEQ or TST, + // the carry flag is updated to bit[31] of the constant, + // if the constant is greater than 255 and can be produced by shifting an 8-bit value. + // The constant is the second operand, or the only one of MOV and + // MVN. The first operand of the others can be a constant too, + // r15 read as a value, and has nothing to do with the carry. + const ArmOp::Operand& constant = (op.op_type == ArmOp::MOV || op.op_type == ArmOp::MVN) ? op.arg[0] : op.arg[1]; + if (constant.isImmediate() && !constant.isShifted() && constant.getImmediate() > 255) + { + set_carry_bit = true; + mov(ecx, (constant.getImmediate() & 0x80000000) >> 31); + } + } + + /* Every instruction here but MOV and MVN has two operands, and they + * are taken by value, into edx and eax, before anything is written: + * the result may be going to the register one of them came from. */ + if (op.op_type != ArmOp::MOV && op.op_type != ArmOp::MVN) + { + if (arg0.isNone()) + mov(edx, op.arg[0].getImmediate()); + else if (arg0 != edx) + mov(edx, arg0); + if (arg1.isNone()) + mov(eax, op.arg[1].getImmediate()); + else if (arg1 != eax) + mov(eax, arg1); + } + if (op.op_type == ArmOp::ADC || op.op_type == ArmOp::SBC || op.op_type == ArmOp::RSC) + { + // the ARM's carry into the host's + bt(armFlags(), 29); + if (op.op_type != ArmOp::ADC) + cmc(); // subtracting, the ARM's carry is "no borrow" + } + + switch (op.op_type) + { + case ArmOp::AND: + and_(edx, eax); + mov(rd, edx); + save_v_flag = false; + break; + case ArmOp::ORR: + or_(edx, eax); + mov(rd, edx); + save_v_flag = false; + break; + case ArmOp::EOR: + xor_(edx, eax); + mov(rd, edx); + save_v_flag = false; + break; + case ArmOp::BIC: + not_(eax); + and_(edx, eax); + mov(rd, edx); + save_v_flag = false; + break; + + case ArmOp::TST: + test(edx, eax); + save_v_flag = false; + break; + case ArmOp::TEQ: + xor_(edx, eax); + save_v_flag = false; + break; + case ArmOp::CMP: + cmp(edx, eax); + if (set_flags) + { + setnb(cl); + set_carry_bit = true; + } + break; + case ArmOp::CMN: + add(edx, eax); + if (set_flags) + { + setb(cl); + set_carry_bit = true; + } + break; + + case ArmOp::MOV: + if (arg0.isNone()) + mov(rd, op.arg[0].getImmediate()); + else if (arg0 != rd) + mov(rd, arg0); + if (set_flags) + { + test(rd, rd); + save_v_flag = false; + } + break; + case ArmOp::MVN: + if (arg0.isNone()) + mov(rd, ~op.arg[0].getImmediate()); + else + { + if (arg0 != rd) + mov(rd, arg0); + not_(rd); + } + if (set_flags) + { + test(rd, rd); + save_v_flag = false; + } + break; + + case ArmOp::SUB: + case ArmOp::SBC: + // rd = rn - op2 (- !C) + if (op.op_type == ArmOp::SUB) + sub(edx, eax); + else + sbb(edx, eax); + if (set_flags) + { + setnb(cl); + set_carry_bit = true; + } + mov(rd, edx); + break; + case ArmOp::RSB: + case ArmOp::RSC: + // rd = op2 - rn (- !C) + if (op.op_type == ArmOp::RSB) + sub(eax, edx); + else + sbb(eax, edx); + if (set_flags) + { + setnb(cl); + set_carry_bit = true; + } + mov(rd, eax); + break; + case ArmOp::ADD: + case ArmOp::ADC: + if (op.op_type == ArmOp::ADD) + add(edx, eax); + else + adc(edx, eax); + if (set_flags) + { + setb(cl); + set_carry_bit = true; + } + mov(rd, edx); + break; + default: + die("invalid"); + break; + } + /* A result that goes to the program counter loses its low two bits: + * the ARM takes instructions from whole words only. (A constant put + * there is the address of an instruction, and has none. None of + * these sets the flags, so the host's are free.) */ + if (op.rd.isReg() && op.rd.getReg().armreg == R15_ARM_NEXT + && !(op.op_type == ArmOp::MOV && arg0.isNone())) + and_(rd, 0xfffffffc); + + return save_v_flag; + } + + void emitMemOp(const ArmOp& op) + { + Xbyak::Operand addr_reg = getOperand(op.arg[0], call_regs[0]); + if (addr_reg != call_regs[0]) + { + if (addr_reg.isNone()) + mov(call_regs[0], op.arg[0].getImmediate()); + else + mov(call_regs[0], addr_reg); + addr_reg = call_regs[0]; + } + if (op.pre_index) + { + const ArmOp::Operand& offset = op.arg[1]; + Xbyak::Operand offset_reg = getOperand(offset, eax); + if (!offset_reg.isNone()) + { + if (op.add_offset) + add(addr_reg, offset_reg); + else + sub(addr_reg, offset_reg); + } + else if (offset.isImmediate() && offset.getImmediate() != 0) + { + if (op.add_offset) + add(addr_reg, offset.getImmediate()); + else + sub(addr_reg, offset.getImmediate()); + } + } + if (op.aligned) + and_(addr_reg, 0xfffffffc); + if (op.op_type == ArmOp::STR) + { + if (op.arg[2].isImmediate()) + mov(call_regs[1], op.arg[2].getImmediate()); + else + mov(call_regs[1], regalloc->map(op.arg[2].getReg().armreg)); + } + + /* Nearly every access is to the ARM's memory, and that is one move + * to or from the host's: it is done here. What is left for the call + * is the sound chip's registers, above 0x800000, and a word read + * from an address that is not a word's, which comes back rotated. */ + Xbyak::Label slow, done; + mov(eax, addr_reg); + and_(eax, 0x00ffffff); + cmp(eax, 0x00800000); + jae(slow); + if (!op.byte_xfer) + { + if (op.op_type == ArmOp::LDR) + { + test(eax, 3); + jnz(slow); + } + else + and_(eax, 0xfffffffc); // a word is stored at a word's address + } + and_(eax, dword[&ARAM_MASK]); + add(eax, dword[&aica_ram.data]); + if (op.op_type == ArmOp::LDR) + { + if (op.byte_xfer) + movzx(eax, byte[eax]); + else + mov(eax, dword[eax]); + } + else + { + if (op.byte_xfer) + mov(byte[eax], dl); + else + mov(dword[eax], call_regs[1]); + } + jmp(done); + L(slow); + call(recompiler::getMemOp(op.op_type == ArmOp::LDR, op.byte_xfer)); + L(done); + + if (op.op_type == ArmOp::LDR) + { + // the program counter has no low two bits + if (op.rd.getReg().armreg == R15_ARM_NEXT) + and_(eax, 0xfffffffc); + mov(regalloc->map(op.rd.getReg().armreg), eax); + } + } + + void saveFlags(bool save_v_flag) + { + if (!set_flags) + return; + + pushf(); + pop(eax); + + if (save_v_flag) + { + mov(edx, eax); + shl(edx, 28 - 11); // V + } + shl(eax, 30 - 6); // Z,N + if (save_v_flag) + and_(edx, V_FLAG); // V + and_(eax, Z_FLAG | N_FLAG); // Z,N + if (save_v_flag) + or_(eax, edx); + + mov(edx, armFlags()); + if (set_carry_bit) + { + if (save_v_flag) + and_(edx, ~(Z_FLAG | N_FLAG | C_FLAG | V_FLAG)); + else + and_(edx, ~(Z_FLAG | N_FLAG | C_FLAG)); + shl(ecx, 29); + or_(edx, ecx); + } + else + { + if (save_v_flag) + and_(edx, ~(Z_FLAG | N_FLAG | V_FLAG)); + else + and_(edx, ~(Z_FLAG | N_FLAG)); + } + or_(edx, eax); + mov(armFlags(), edx); + } + + void emitBranch(const ArmOp& op) + { + Xbyak::Operand addr_reg = getOperand(op.arg[0], eax); + if (addr_reg.isNone()) + mov(eax, op.arg[0].getImmediate()); + else + { + if (eax != addr_reg) + mov(eax, addr_reg); + and_(eax, 0xfffffffc); + } + mov(dword[&arm_Reg[R15_ARM_NEXT].I], eax); + } + + void emitMSR(const ArmOp& op) + { + if (op.arg[0].isImmediate()) + mov(call_regs[0], op.arg[0].getImmediate()); + else + mov(call_regs[0], regalloc->map(op.arg[0].getReg().armreg)); + mov(call_regs[1], op.psrMask); + if (op.spsr) + call((const void *)&recompiler::MSR_do<1>); + else + call((const void *)&recompiler::MSR_do<0>); + } + + void emitMRS(const ArmOp& op) + { + call((const void *)&CPUUpdateCPSR); + + if (op.spsr) + mov(regalloc->map(op.rd.getReg().armreg), dword[&arm_Reg[RN_SPSR].I]); + else + mov(regalloc->map(op.rd.getReg().armreg), dword[&arm_Reg[RN_CPSR].I]); + } + + /* On to the next block, by a jump of the block's own (see + * arm7_rec_x64.cpp): through the address's entry point when the block + * ends by going to a fixed address, else through the table. The + * shared dispatcher takes over when the time slice is used up or an + * interrupt is waiting. */ + void emitDispatch(const std::vector& block_ops, u32 cycles) + { + /* The block's cycles come off here, at its end, and the slice is + * over at none left or less: the subtraction leaves the flags the + * jump wants. */ + sub(dword[&arm_Reg[CYCL_CNT].I], cycles); + jle((const void *)arm_dispatch); + cmp(dword[&arm_Reg[INTR_PEND].I], 0); + jne((const void *)arm_dispatch); + + const ArmOp *last = block_ops.empty() ? nullptr : &block_ops.back(); + if (last != nullptr && last->condition == ArmOp::AL && last->arg[0].isImmediate() && !last->arg[0].isShifted() + && ((last->op_type == ArmOp::B || last->op_type == ArmOp::BL) + || (last->op_type == ArmOp::MOV && last->rd.isReg() && last->rd.getReg().armreg == R15_ARM_NEXT))) + { + const u32 target = last->arg[0].getImmediate(); + jmp(dword[&recompiler::EntryPoints[(target & (ARAM_SIZE_MAX - 1)) / 4]]); + } + else + { + mov(ecx, dword[&arm_Reg[R15_ARM_NEXT].I]); + mov(edx, dword[&entry_points]); + and_(ecx, 0x7ffffc); + jmp(dword[edx + ecx]); // four bytes to an entry point, one for each word + } + } + + void emitFallback(const ArmOp& op) + { + set_flags = false; + mov(call_regs[0], op.arg[0].getImmediate()); + call((const void *)&recompiler::interpret); + } + +public: + Arm7Compiler() : Xbyak::CodeGenerator(recompiler::spaceLeft(), recompiler::currentCode()) { } + + void compile(const std::vector& block_ops, u32 cycles) + { + X86ArmRegAlloc block_regalloc(*this, block_ops); + regalloc = &block_regalloc; + + ArmOp::Condition currentCondition = ArmOp::AL; + Xbyak::Label *condLabel = nullptr; + + for (u32 i = 0; i < block_ops.size(); i++) + { + const ArmOp& op = block_ops[i]; + DEBUG_LOG(AICA_ARM, "-> %s", op.toString().c_str()); + + set_flags = op.flags & ArmOp::OP_SETS_FLAGS; + logical_op_set_flags = op.isLogicalOp() && set_flags; + set_carry_bit = false; + bool save_v_flag = true; + + if (op.op_type == ArmOp::FALLBACK) + { + endConditional(condLabel); + condLabel = nullptr; + currentCondition = ArmOp::AL; + } + else if (op.condition != currentCondition) + { + endConditional(condLabel); + currentCondition = op.condition; + if (op.condition != ArmOp::AL) + regalloc->storeDirty(); + condLabel = startConditional(op.condition); + } + + regalloc->load(i); + + if (op.op_type <= ArmOp::MVN) + // data processing op + save_v_flag = emitDataProcOp(op); + else if (op.op_type <= ArmOp::STR) + // memory load/store + emitMemOp(op); + else if (op.op_type <= ArmOp::BL) + // branch + emitBranch(op); + else if (op.op_type == ArmOp::MRS) + emitMRS(op); + else if (op.op_type == ArmOp::MSR) + emitMSR(op); + else if (op.op_type == ArmOp::FALLBACK) + emitFallback(op); + else + die("invalid"); + + saveFlags(save_v_flag); + + regalloc->store(i); + + /* The next instruction's condition is for the flags as this one + * leaves them, and writing the status register changes them as + * surely as an instruction that sets them. */ + if (set_flags || op.op_type == ArmOp::MSR) + { + currentCondition = ArmOp::AL; + endConditional(condLabel); + condLabel = nullptr; + } + } + endConditional(condLabel); + + emitDispatch(block_ops, cycles); + + ready(); + recompiler::advance(getSize()); + + regalloc = nullptr; + } + + void generateMainLoop() + { + if (!recompiler::empty()) + { + verify(arm_mainloop != nullptr); + verify(arm_compilecode != nullptr); + return; + } + Xbyak::Label arm_dispatch_label; + Xbyak::Label arm_mainloop_label; + + //arm_compilecode: + call((const void *)&recompiler::compile); + jmp(arm_dispatch_label, T_NEAR); + + // arm_mainloop: called from C, with its arguments on the stack + L(arm_mainloop_label); + mov(eax, dword[esp + 8]); // entrypoints + mov(dword[&entry_points], eax); + push(ebp); + push(ebx); + push(esi); + push(edi); + // (the caller's stack pointer, under the place it is aligned to) + mov(eax, esp); + and_(esp, ~15); + sub(esp, 12); + push(eax); + + // arm_dispatch: + L(arm_dispatch_label); + mov(edx, dword[&entry_points]); + mov(ecx, dword[&arm_Reg[R15_ARM_NEXT].I]); + mov(eax, dword[&arm_Reg[INTR_PEND].I]); + cmp(dword[&arm_Reg[CYCL_CNT].I], 0); + Xbyak::Label arm_exit; + jle(arm_exit, T_NEAR); // timeslice is over + test(eax, eax); + Xbyak::Label arm_dofiq; + jne(arm_dofiq, T_NEAR); // if interrupt pending, handle it + + and_(ecx, 0x7ffffc); + jmp(dword[edx + ecx]); + + // arm_dofiq: + L(arm_dofiq); + call((const void *)&CPUFiq); + jmp(arm_dispatch_label, T_NEAR); + + // arm_exit: + L(arm_exit); + pop(esp); + pop(edi); + pop(esi); + pop(ebx); + pop(ebp); + ret(); + + ready(); + arm_compilecode = (void (*)())getCode(); + arm_mainloop = (arm_mainloop_t)arm_mainloop_label.getAddress(); + arm_dispatch = (void (*)())arm_dispatch_label.getAddress(); + + recompiler::advance(getSize()); + } + +}; + +void X86ArmRegAlloc::LoadReg(int host_reg, Arm7Reg armreg) +{ + assembler.mov(getReg32(host_reg), dword[&arm_Reg[(u32)armreg].I]); +} + +void X86ArmRegAlloc::StoreReg(int host_reg, Arm7Reg armreg) +{ + assembler.mov(dword[&arm_Reg[(u32)armreg].I], getReg32(host_reg)); +} + +void arm7backend_compile(const std::vector& block_ops, u32 cycles) +{ + Arm7Compiler assembler; + assembler.compile(block_ops, cycles); +} + +void arm7backend_flush() +{ + Arm7Compiler assembler; + assembler.generateMainLoop(); +} + +} // namespace aicaarm +#endif // X86 && DYNAREC_JIT diff --git a/core/libretro/libretro_core_options.h b/core/libretro/libretro_core_options.h index 94bdbb62..18ec4f32 100644 --- a/core/libretro/libretro_core_options.h +++ b/core/libretro/libretro_core_options.h @@ -381,7 +381,7 @@ struct retro_core_option_v2_definition option_defs_us[] = { CORE_OPTION_NAME "_arm7_recompiler", "Sound CPU Recompiler", NULL, - "Run the sound chip's ARM7 processor through the recompiler. Disable to run it through the slower interpreter instead, to find out whether a sound problem comes from the recompiler. Either way it runs the same on x86-64 and ARM64. Takes effect at once.", + "Run the sound chip's ARM7 processor through the recompiler. Disable to run it through the slower interpreter instead, to find out whether a sound problem comes from the recompiler. Either way it runs the same on x86-64, 32-bit x86 and ARM64. Takes effect at once.", NULL, NULL, { diff --git a/tools/arm7/run.sh b/tools/arm7/run.sh index d7bd9d46..fd7efe20 100644 --- a/tools/arm7/run.sh +++ b/tools/arm7/run.sh @@ -3,8 +3,9 @@ # # sh tools/arm7/run.sh [programs] # -# On x86-64 and ARM64 the ARM7 is run by a recompiler (core/hw/arm7/ -# arm7_rec.cpp, with arm7_rec_x64.cpp or arm7_rec_arm64.cpp); everywhere the +# On x86-64, 32-bit x86 and ARM64 the ARM7 is run by a recompiler (core/hw/ +# arm7/arm7_rec.cpp, with arm7_rec_x64.cpp, arm7_rec_x86.cpp or +# arm7_rec_arm64.cpp); everywhere the # recompilers do not reach, and for the instructions this one hands back, by # an interpreter (arm7.cpp with arm-new.h). arm7_test.cpp is built around each, with nothing else of the # core, and both are given the same made-up programs, 10000 unless a number @@ -25,8 +26,13 @@ # (MSR CPSR_c), between the modes with banked registers: other programs. # # Needs a C++ compiler and the core's makefile for its flags, and only -# does anything on x86-64 and ARM64: there is no recompiler to compare -# elsewhere. For another processor's build, run under qemu: +# does anything on x86-64, 32-bit x86 and ARM64: there is no recompiler to +# compare elsewhere. For a 32-bit x86 build on an x86-64 machine: +# +# ARCH=i686 MAKEARGS="platform=unix ARCH=i686 CC=i686-linux-gnu-gcc \ +# CXX=i686-linux-gnu-g++" sh tools/arm7/run.sh +# +# For another processor's build, run under qemu: # # ARCH=aarch64 MAKEARGS="platform=arm64 CC=aarch64-linux-gnu-gcc \ # CXX=aarch64-linux-gnu-g++" RUN="qemu-aarch64 -L /usr/aarch64-linux-gnu" \ @@ -37,8 +43,9 @@ cd "$ROOT" PROGRAMS=${1:-10000} case ${ARCH:-$(uname -m)} in x86_64|amd64) BACKEND=core/hw/arm7/arm7_rec_x64.cpp ;; + i386|i686) BACKEND=core/hw/arm7/arm7_rec_x86.cpp ;; aarch64|arm64) BACKEND="core/hw/arm7/arm7_rec_arm64.cpp $(echo core/deps/vixl/*.cc core/deps/vixl/aarch64/*.cc)" ;; - *) echo "arm7: skipped (the recompilers under test are for x86-64 and ARM64)"; exit 0 ;; + *) echo "arm7: skipped (the recompilers under test are for x86-64, 32-bit x86 and ARM64)"; exit 0 ;; esac WORK=${TMPDIR:-/tmp}/arm7_test.$$ mkdir -p "$WORK"