arm7: an ARM7 recompiler for 32-bit x86, the same as x86-64's

Linux i686, Windows i686 and Android x86 ran the sound processor through
the interpreter: the old reicast recompiler's x86 backend is gone, and
upstream flycast has none. They now run the recompiler x86-64 and ARM64
run - arm7_rec.cpp's decoder, blocks, cycle counts and register
allocator - with a backend of its own, arm7_rec_x86.cpp, written with
xbyak in 32-bit mode after arm7_rec_x64.cpp, instruction for instruction:

- a block's cycles come off at its end, and the time slice is over at
  none left or less; each block jumps on to the next itself, through the
  address's entry point when it is fixed;
- the flags stay in the ARM's register file; a logical instruction that
  sets them leaves V and takes the shifter's or the constant's carry; RSB
  and RSC take their carry from the subtraction; a shift by a register
  takes its low byte;
- registers are written back before conditional code and around MSR, as
  the shared allocator does for every backend;
- loads and stores to sound memory are done in line, the rest through
  the same calls; the program counter loses its low two bits wherever it
  is written.

What differs is the processor: four host registers for the ARM's (ebx,
ebp, esi, edi, which C code keeps), eax/ecx/edx for the rest - the
operands in edx and eax, the carry in ecx, the values a shift by 32 or
more gives taken from memory by cmov, RRX done with bt and rcr; data
reached at its absolute address; every call fastcall (ecx, edx), as
every handler called is DYNACALL, with the stack at a multiple of 16;
four bytes to an entry point. The main loop is written into the code
buffer as on x86-64, so the library has no text relocations.

build.h turns the ARM7 recompiler on for 32-bit x86 (so the Makefile's
i686 unix and win32 builds and jni/Android.mk's x86 have it, through
Makefile.common), and the Sound CPU Recompiler core option with it,
whose text now names 32-bit x86. tools/arm7/run.sh runs on i686 too
(ARCH=i686 with the cross compiler in MAKEARGS).

Tested here:
- tools/arm7 on i686, 10000 programs (8844 compared) and 10000 with mode
  changes (8694 compared): the recompiler, the interpreter and switching
  between the two agree; what the recompiled programs leave is the same
  as on x86-64 and on AArch64 (qemu), line for line, in both sets
- tools/threads/headless.sh passes on Linux i686, on Windows i686 under
  32-bit wine, and on Linux x86-64
- tools/determinism, default options (the ARM7 recompiler on on both),
  Jet Grind Radio from a state, 1000 frames: i686 the same as x86-64 at
  every frame
- Android x86 (NDK r27d) builds and links with no TEXTREL
- speed, Linux i686, headless, Jet Grind Radio 1801 frames from a state,
  two runs each: 3.30-3.40 s with the recompiler, 4.21-4.29 s with the
  interpreter (about 540 against 425 frames a second)

No save-state change. Blocks are compiled as before, when first reached;
nothing is allocated or waited for while they run.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-10-23 21:11:57 +02:00
1 parent bbbce3a8c8
commit 802e144624
7 files changed
+966 -17

No files matched your search

+1
View File
@@ -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 \
\
+5 -4
View File
@@ -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
+6 -6
View File
@@ -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
+1 -1
View File
@@ -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"
+940
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
/* 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<Xbyak::Reg32, 4> alloc_regs {
ebx, ebp, esi, edi
};
class X86ArmRegAlloc : public ArmRegAlloc<ALLOC_REGS, X86ArmRegAlloc>
{
using super = ArmRegAlloc<ALLOC_REGS, X86ArmRegAlloc>;
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<ArmOp>& 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<ArmOp>& 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<ArmOp>& 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<ArmOp>& block_ops, u32 cycles)
{
Arm7Compiler assembler;
assembler.compile(block_ops, cycles);
}
void arm7backend_flush()
{
Arm7Compiler assembler;
assembler.generateMainLoop();
}
} // namespace aicaarm
#endif // X86 && DYNAREC_JIT
+1 -1
View File
@@ -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,
{
+12 -5
View File
@@ -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"