mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
eeFprSyncSlotFormat keys on CHECK_FPU_FULL, so the relocated file is what both of iFPUd's rungs compute over, not mode 3 alone. The comments that name a mode where they mean the tier are renamed to the tier.
1529 lines
58 KiB
C++
1529 lines
58 KiB
C++
// SPDX-FileCopyrightText: 2026 yaps2 Dev Team
|
||
// SPDX-License-Identifier: GPL-3.0+
|
||
|
||
// ARM64 EE FPU (COP1) Instruction Codegen — NEON-based
|
||
// Transfer ops (MFC1/MTC1/CFC1/CTC1): native with NEON allocation.
|
||
// Branch ops (BC1F/BC1T): native, read fprc[31] directly.
|
||
// Arithmetic ops: native NEON with PS2 clamping and guard-bit ADD/SUB emulation
|
||
// (fast single-precision path here; the accuracy-mode DOUBLE path is in
|
||
// iFPUd-arm64.cpp, selected when CHECK_FPU_FULL). All ops including DIV/SQRT/
|
||
// RSQRT are native; nothing here defers to the interpreter.
|
||
|
||
#include "arm64/iR5900-arm64.h"
|
||
|
||
#include <cfloat>
|
||
|
||
namespace a64 = vixl::aarch64;
|
||
|
||
namespace R5900 {
|
||
namespace Dynarec {
|
||
namespace OpcodeImpl {
|
||
namespace COP1 {
|
||
|
||
namespace Interp = R5900::Interpreter::OpcodeImpl::COP1;
|
||
|
||
|
||
#define _Ft_ _Rt_
|
||
#define _Fs_ _Rd_
|
||
#define _Fd_ _Sa_
|
||
|
||
#define FPUflagC 0x00800000
|
||
#define FPUflagI 0x00020000
|
||
#define FPUflagD 0x00010000
|
||
#define FPUflagO 0x00008000
|
||
#define FPUflagU 0x00004000
|
||
#define FPUflagSI 0x00000040
|
||
#define FPUflagSD 0x00000020
|
||
|
||
//------------------------------------------------------------------
|
||
// FCR31 block residency (GE-12)
|
||
//------------------------------------------------------------------
|
||
// The leak/4248 guest-class-0xc design: the C.cond/BC1/CFC1/CTC1/DIV/SQRT/
|
||
// RSQRT family accesses fprc[31] through the GPR allocator (ARM64TYPE_FPRC —
|
||
// the load/writeback plumbing existed in iCore but had no allocation site).
|
||
// C.cond becomes Fcmp+Cset+Bfi on the resident reg, BC1 a Tbnz on it (zero
|
||
// loads after a preceding compare), and the DIV/SQRT flag RMWs lose their
|
||
// Ldr/Str round-trips. Every iFlushCall seam writes the slot back (FPRC
|
||
// lives in a caller-saved-only pool — see _allocArm64GPR), and recCall's
|
||
// FLUSH_INTERPRETER fully evicts it (e.g. the recRSQRT_S fallback, whose
|
||
// interp body RMWs fprc in C), so fprc[31] memory is canonical wherever C
|
||
// code or another block can look.
|
||
//
|
||
// Returns -1 under CHECK_FPU_FULL: the DOUBLE:: bodies (iFPUd-arm64.cpp)
|
||
// RMW fprc[31] memory raw, and these entry points are shared between modes —
|
||
// mixing a resident copy with raw-memory RMWs inside one block would desync.
|
||
// Under FULL every site below keeps today's raw-memory shape (GE-20 owns
|
||
// FULL-mode depth).
|
||
static int fpuTryAllocFCR31(int mode)
|
||
{
|
||
if (CHECK_FPU_FULL)
|
||
return -1;
|
||
return _allocArm64GPR(ARM64TYPE_FPRC, 31, mode);
|
||
}
|
||
|
||
//------------------------------------------------------------------
|
||
// CFC1 — rt = fprc[fs] (read FPU control register)
|
||
//------------------------------------------------------------------
|
||
void recCFC1()
|
||
{
|
||
if (!_Rt_) return;
|
||
|
||
_deleteEEreg(_Rt_, 0);
|
||
GPR_DEL_CONST(_Rt_);
|
||
|
||
if (_Fs_ >= 16)
|
||
{
|
||
// FCR31: mask out always-zero bits, set always-one bits (x86 recCFC1
|
||
// shape; the interpreter's CFC1 applies the same model, so this is
|
||
// not a divergence — pinned by
|
||
// EeRecFpu.CompareThenCfc1SeesFreshConditionBit and
|
||
// EeFpuFcrConsoleConformance.BothEnginesMatchConsoleFcrModel).
|
||
const int fl = fpuTryAllocFCR31(MODE_READ);
|
||
if (fl >= 0)
|
||
armAsm->And(RWSCRATCH, armWRegister(fl), 0x0083c078);
|
||
else
|
||
{
|
||
armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||
armAsm->And(RWSCRATCH, RWSCRATCH, 0x0083c078);
|
||
}
|
||
armAsm->Orr(RWSCRATCH, RWSCRATCH, 0x01000001);
|
||
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
|
||
}
|
||
else
|
||
{
|
||
// FCR0: read-only revision register
|
||
armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[0]);
|
||
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
|
||
}
|
||
// Deposit last, after any FCR31 slot allocation above: the dest home is
|
||
// resolved at the store so an allocator-resident rt slot can't be evicted
|
||
// between resolve and use.
|
||
_eeStoreGPRDestReg(_Rt_, RXSCRATCH);
|
||
}
|
||
|
||
//------------------------------------------------------------------
|
||
// CTC1 — fprc[fs] = rt (write FPU control register)
|
||
//------------------------------------------------------------------
|
||
void recCTC1()
|
||
{
|
||
if (_Fs_ != 31) return;
|
||
|
||
const int fl = fpuTryAllocFCR31(MODE_WRITE);
|
||
if (fl >= 0)
|
||
{
|
||
// Full-register write: no read-in, just materialize rt into the slot.
|
||
// _eeMoveGPRtoR reads const/pin/allocator-resident rt directly (the
|
||
// old shape forced rt to memory with _deleteEEreg and reloaded it).
|
||
_eeMoveGPRtoR(armWRegister(fl), _Rt_);
|
||
}
|
||
else
|
||
{
|
||
a64::Register rt = RWSCRATCH;
|
||
if (GPR_IS_CONST1(_Rt_))
|
||
armAsm->Mov(RWSCRATCH, g_cpuConstRegs[_Rt_].UL[0]);
|
||
else
|
||
{
|
||
_deleteEEreg(_Rt_, 1);
|
||
rt = _eeGetGPRSourceReg(RWSCRATCH, _Rt_);
|
||
}
|
||
armStoreEERegPtr(rt, &fpuRegs.fprc[_Fs_]);
|
||
}
|
||
}
|
||
|
||
//------------------------------------------------------------------
|
||
// MFC1 — rt = sign_extend(fpr[fs]) (move 32-bit float to GPR)
|
||
//------------------------------------------------------------------
|
||
void recMFC1()
|
||
{
|
||
if (!_Rt_) return;
|
||
|
||
_deleteEEreg(_Rt_, 0);
|
||
GPR_DEL_CONST(_Rt_);
|
||
|
||
// FPR-side allocator coherence: fpr[fs] may be live in NEON (e.g. a
|
||
// preceding ADD_S wrote it, possibly MODE_WRITE-only). If it is already
|
||
// resident, read it straight from the host reg instead of flushing it to
|
||
// memory and reloading (store→load-forward stall on A53).
|
||
// MFC1 doesn't modify fpr[fs], so leave the allocator slot intact. Only
|
||
// the not-resident case falls back to the memory load.
|
||
const int fsreg = _checkNEONreg(NEONTYPE_FPREG, _Fs_, MODE_READ);
|
||
if (fsreg >= 0)
|
||
{
|
||
armEmitEeFprWordFromSlot(RWSCRATCH, armDRegister(fsreg), a64::x9);
|
||
}
|
||
else
|
||
{
|
||
armEmitEeFprWordFromSlotMem(RWSCRATCH, armCpuRegMem(&fpuRegs.fpr[_Fs_]), a64::x9);
|
||
}
|
||
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
|
||
// Deposit last, after the FPR-slot probe above (see recCFC1).
|
||
_eeStoreGPRDestReg(_Rt_, RXSCRATCH);
|
||
}
|
||
|
||
//------------------------------------------------------------------
|
||
// MTC1 — fpr[fs] = rt[31:0] (move GPR lower 32 bits to FPR)
|
||
//------------------------------------------------------------------
|
||
void recMTC1()
|
||
{
|
||
a64::Register rt = RWSCRATCH;
|
||
if (GPR_IS_CONST1(_Rt_))
|
||
armAsm->Mov(RWSCRATCH, g_cpuConstRegs[_Rt_].UL[0]);
|
||
else
|
||
{
|
||
_deleteEEreg(_Rt_, 1);
|
||
rt = _eeGetGPRSourceReg(RWSCRATCH, _Rt_);
|
||
}
|
||
|
||
// If fpr[fs] is already resident in NEON, write the new bits straight into
|
||
// the host reg and mark it dirty (MODE_WRITE), keeping it hot for a
|
||
// following FPU op; the block epilogue flushes the host reg to fpr[fs].
|
||
// MTC1 overwrites fpr[fs] wholesale, so any prior MODE_WRITE-only value
|
||
// in the slot is dead and correctly discarded by overwriting it.
|
||
// GE-11: when fs is NOT resident but the backprop analysis says it is
|
||
// used later in the block, ALLOCATE the destination slot (write-only, no
|
||
// memory load) — this is the previously-dead _allocIfUsedFPUtoNEON, the
|
||
// x86 recMTC1 model. Kills the Str+Ldr idiom between MTC1 and the
|
||
// following CVT.S/arith. Unused-dest falls back to the memory store.
|
||
int fsreg = _checkNEONreg(NEONTYPE_FPREG, _Fs_, MODE_WRITE);
|
||
if (fsreg < 0)
|
||
fsreg = _allocIfUsedFPUtoNEON(_Fs_, MODE_WRITE);
|
||
if (fsreg >= 0)
|
||
{
|
||
armEmitEeFprSlotFromWord(armDRegister(fsreg), rt, RXSCRATCH);
|
||
}
|
||
else
|
||
{
|
||
armEmitEeFprSlotMemFromWord(armCpuRegMem(&fpuRegs.fpr[_Fs_]), rt, RXSCRATCH);
|
||
}
|
||
}
|
||
|
||
//------------------------------------------------------------------
|
||
// BC1F / BC1T — branch on FPU condition flag
|
||
//------------------------------------------------------------------
|
||
|
||
// FPU branch setup: flush state and test fprc[31] condition flag.
|
||
// Emits conditional forward branch (skip label), matching EE branch pattern.
|
||
// bne=false: BC1F (skip if C set), bne=true: BC1T (skip if C clear)
|
||
static a64::Label* s_pBC1Label = nullptr;
|
||
|
||
static void recSetBranchBC1(bool branchOnTrue)
|
||
{
|
||
_eeFlushAllDirty();
|
||
|
||
// GE-12: _eeFlushAllDirty writes back but KEEPS residency, so when an
|
||
// earlier op in the block touched FCR31 (C.cond, DIV, CTC1, ...) the
|
||
// common compare→branch chain pays zero loads here; not-resident
|
||
// allocates with one Ldr, same cost as the old raw load.
|
||
a64::Register flagReg = RWSCRATCH;
|
||
const int fl = fpuTryAllocFCR31(MODE_READ);
|
||
if (fl >= 0)
|
||
flagReg = armWRegister(fl);
|
||
else
|
||
armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||
|
||
// FPUflagC (0x00800000) is a single fixed bit (23), so the Tst+B.cond pair
|
||
// collapses to one test-bit-and-branch (Tbnz/Tbz). The forward branch
|
||
// skips the delay slot on the not-taken edge.
|
||
static_assert(FPUflagC == (1u << 23), "FPUflagC must be a single bit for Tbz/Tbnz");
|
||
s_pBC1Label = new a64::Label();
|
||
if (branchOnTrue)
|
||
armAsm->Tbz(flagReg, 23, s_pBC1Label); // BC1T: skip taken if C clear
|
||
else
|
||
armAsm->Tbnz(flagReg, 23, s_pBC1Label); // BC1F: skip taken if C set
|
||
}
|
||
|
||
static void recBindBC1Label()
|
||
{
|
||
armAsm->Bind(s_pBC1Label);
|
||
delete s_pBC1Label;
|
||
s_pBC1Label = nullptr;
|
||
}
|
||
|
||
void recBC1F()
|
||
{
|
||
u32 branchTo = ((s32)_Imm_ * 4) + pc;
|
||
|
||
const bool swap = TrySwapDelaySlot(0, 0, 0, true);
|
||
recSetBranchBC1(false);
|
||
|
||
if (!swap)
|
||
{
|
||
SaveBranchState();
|
||
recompileNextInstruction(true, false);
|
||
}
|
||
SetBranchImm(branchTo);
|
||
|
||
recBindBC1Label();
|
||
|
||
if (!swap)
|
||
{
|
||
pc -= 4;
|
||
LoadBranchState();
|
||
recompileNextInstruction(true, false);
|
||
}
|
||
SetBranchImm(pc);
|
||
}
|
||
|
||
void recBC1T()
|
||
{
|
||
u32 branchTo = ((s32)_Imm_ * 4) + pc;
|
||
|
||
const bool swap = TrySwapDelaySlot(0, 0, 0, true);
|
||
recSetBranchBC1(true);
|
||
|
||
if (!swap)
|
||
{
|
||
SaveBranchState();
|
||
recompileNextInstruction(true, false);
|
||
}
|
||
SetBranchImm(branchTo);
|
||
|
||
recBindBC1Label();
|
||
|
||
if (!swap)
|
||
{
|
||
pc -= 4;
|
||
LoadBranchState();
|
||
recompileNextInstruction(true, false);
|
||
}
|
||
SetBranchImm(pc);
|
||
}
|
||
|
||
void recBC1FL()
|
||
{
|
||
u32 branchTo = ((s32)_Imm_ * 4) + pc;
|
||
|
||
recSetBranchBC1(false);
|
||
|
||
SaveBranchState();
|
||
recompileNextInstruction(true, false);
|
||
SetBranchImm(branchTo);
|
||
|
||
recBindBC1Label();
|
||
LoadBranchState();
|
||
SetBranchImm(pc);
|
||
}
|
||
|
||
void recBC1TL()
|
||
{
|
||
u32 branchTo = ((s32)_Imm_ * 4) + pc;
|
||
|
||
recSetBranchBC1(true);
|
||
|
||
SaveBranchState();
|
||
recompileNextInstruction(true, false);
|
||
SetBranchImm(branchTo);
|
||
|
||
recBindBC1Label();
|
||
LoadBranchState();
|
||
SetBranchImm(pc);
|
||
}
|
||
|
||
#undef _Ft_
|
||
#undef _Fs_
|
||
#undef _Fd_
|
||
|
||
|
||
#define _Ft_ _Rt_
|
||
#define _Fs_ _Rd_
|
||
#define _Fd_ _Sa_
|
||
|
||
// PS2 FPU max representable float (no infinity) — exactly ±FLT_MAX
|
||
// under IEEE-754 single precision: 0x7F7FFFFF / 0xFF7FFFFF.
|
||
// Load-bearing for the EE/mVU dispatcher prologues that park these bit
|
||
// patterns in s8/s9 — keep this assert if the type of FLT_MAX ever changes.
|
||
static_assert(FLT_MAX == 3.40282346638528859811704183484516925e+38f,
|
||
"FLT_MAX must be the IEEE-754 0x7F7FFFFF bit pattern (PS2 FPU clamp upper bound).");
|
||
|
||
// Clamp a result in `fpr` to PS2 float range (no inf/nan).
|
||
//
|
||
// Branchless Fminnm/Fmaxnm: the Number variants are NaN-eating (matching
|
||
// x86 MINSS/MAXSS), so NaN routes through Fminnm to +max and Fmaxnm
|
||
// passes it through. Both ±Inf get clamped to ±max.
|
||
//
|
||
// The ±FLT_MAX bounds live in callee-saved s8/s9 — loaded once at JIT
|
||
// session entry in `_DynGen_EnterRecompiledCode` and held across every
|
||
// armEmitCall via AAPCS64. v8/v9 are excluded from the NEON allocator
|
||
// pool so no codegen path can clobber them. 2 host insns per clamp.
|
||
//
|
||
// NaN sign is not preserved (matches x86 fpuFloat / ClampValues; the
|
||
// PS2 FPU has no NaN concept so this is design-correct).
|
||
static void fpuClampResult(const a64::VRegister& fpr)
|
||
{
|
||
armAsm->Fminnm(fpr, fpr, a64::s8);
|
||
armAsm->Fmaxnm(fpr, fpr, a64::s9);
|
||
}
|
||
|
||
// This file used to carry two more clamp helpers, both now deleted along with
|
||
// their only callers:
|
||
//
|
||
// fpuClampOperandPositive — an integer Umin against +fMax, for SQRT.S's
|
||
// post-Fabs operand. recSQRT_S_xmm scales by a power of two instead.
|
||
// fpuClampResultPositive — a one-sided Fminnm against +fMax, for ABS.S.
|
||
// recABS_S_xmm emits nothing but the Fabs now.
|
||
//
|
||
// All three removals are the same finding: exponent 255 is an ORDINARY binade
|
||
// on the EE, so clamping those patterns to +fMax is not saturation, it is
|
||
// corruption. Both interpreters and the console pass them through untouched.
|
||
// See EeFpuAbsNegClamp and EeFpuOverflowConsole.SqrtMatchesConsoleOnEvery*.
|
||
|
||
// Clear the O and U cause flags.
|
||
//
|
||
// This is a whole-family obligation, not an ABS/NEG one. The EE clears the two
|
||
// CAUSE bits (the sticky SO/SU survive) on every op that can raise them: the
|
||
// ten raiseOrClearOU ops in FPU.cpp — ADD, SUB, MUL and the A-forms, plus the
|
||
// four multiply-accumulates — and MAX/MIN/ABS/NEG, which clearFPUFlags(O|U)
|
||
// and do nothing else. DIV, SQRT, RSQRT, MOV, CVT and the compares must leave
|
||
// both alone, and do not call this.
|
||
//
|
||
// The fast path cleared none of them, so an O or U raised by an earlier
|
||
// instruction stayed visible to the next cfc1 for the rest of the block.
|
||
// Measured against silicon on the FCR31-seeded capture rows: ABS, NEG, ADD,
|
||
// ADDA, MADD, MSUB, MUL, MULA, MAX and MIN all read back 0x0183C079 where the
|
||
// console gives 0x01830079. SUB, SUBA, MADDA and MSUBA have no seeded row and
|
||
// are fixed on the interpreter's authority alone (raiseOrClearOU clears both
|
||
// causes on every one of the ten). x86 iFPU.cpp has the identical defect —
|
||
// the same line is commented out at 13 sites.
|
||
//
|
||
// It goes FIRST in each emitter, before the op writes anything. The fast path
|
||
// raises neither flag today so the order is not yet observable, but the
|
||
// interpreter and the FULL path both clear-then-set, and an emitter that
|
||
// learns to raise O must not have its flag wiped by a clear placed after it.
|
||
//
|
||
// The clear is free; RAISING O/U is not, and that asymmetry is the tier line.
|
||
// A correct raise needs the exact magnitude of the result, which a saturating
|
||
// single cannot carry — see ToPS2FPU_Full (iFPUd-arm64.cpp), which gets it
|
||
// only because fpuFullMode already pays for double arithmetic.
|
||
//
|
||
// Pinned by EeFpuAbsNegClamp.AbsAndNegClearOverflowFlags and
|
||
// EeFpuFcrConsoleConformance.EnginesAgreeOnTheOverflowFlagClear.
|
||
//
|
||
// GE-12: honour a register-allocated FCR31 rather than going through memory,
|
||
// same shape as recSQRT_S_xmm's flag RMW.
|
||
static void fpuClearOUFlags()
|
||
{
|
||
const int fl = fpuTryAllocFCR31(MODE_READ | MODE_WRITE);
|
||
const a64::Register flagReg = (fl >= 0) ? armWRegister(fl) : RWSCRATCH;
|
||
if (fl < 0)
|
||
armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||
armAsm->Bic(flagReg, flagReg, FPUflagO | FPUflagU);
|
||
if (fl < 0)
|
||
armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||
}
|
||
|
||
// Sign-preserving operand clamp for FPU comparisons (C.cond.S).
|
||
//
|
||
// Mirrors the x86 JIT's fpuFloat3 (PMIN.SD vs 0x7f7fffff then PMIN.UD vs
|
||
// 0xff7fffff): +NaN->+fMax, -NaN->-fMax, +Inf->+fMax, -Inf->-fMax. The PS2
|
||
// FPU has no Inf/NaN concept, so both compare operands must be clamped first;
|
||
// a raw Fcmp on an unclamped NaN would go unordered (all-false) where the PS2
|
||
// wants an ordered compare against ±FLT_MAX.
|
||
//
|
||
// Integer SMIN/UMIN preserve the sign bit — unlike fpuClampResult's
|
||
// Fminnm/Fmaxnm, which fold every NaN to +fMax and would mis-order -NaN.
|
||
// s8/s9 already hold the 0x7f7fffff / 0xff7fffff bit patterns. Only lane 0
|
||
// (the scalar S reg) is consumed by the following Fcmp, so the upper V4S
|
||
// lanes are don't-care. (Denormal flush-to-zero is intentionally omitted to
|
||
// match fpuFloat3; that is a pre-existing shared JIT-vs-interp behavior.)
|
||
static void fpuClampCompareOperand(const a64::VRegister& s)
|
||
{
|
||
const a64::VRegister v(s.GetCode(), a64::kQRegSize);
|
||
armAsm->Smin(v.V4S(), v.V4S(), a64::VRegister(8, a64::kQRegSize).V4S());
|
||
armAsm->Umin(v.V4S(), v.V4S(), a64::VRegister(9, a64::kQRegSize).V4S());
|
||
}
|
||
|
||
// Source-operand clamp for the FPU arithmetic family, gated on
|
||
// CHECK_FPU_EXTRA_OVERFLOW (per-game GameDB clampMode>=2). When enabled, the
|
||
// PS2 FPU recs clamp each fpr source to ±fMax *before* the op — matching the
|
||
// interpreter (which routes every operand through fpuDouble) and x86
|
||
// recCommutativeOp/recMADDtemp (fpuFloat2 under the same gate). This catches
|
||
// Inf*0 -> NaN / (+Inf)+(-Inf) -> NaN poison where an fpr was filled with raw
|
||
// Inf/NaN bits via MOV.S/LWC1/MTC1; without it the op produces a NaN that
|
||
// the result clamp folds to +fMax, diverging from the interpreter's
|
||
// clamp-then-compute (e.g. fMax*0 = 0).
|
||
//
|
||
// Copies into `scratch` rather than mutating the allocator-resident source
|
||
// (vs x86's in-place fpuFloat2) so a later read of the same fpr in this block
|
||
// still sees the unclamped value. Sign-preserving (fpuClampCompareOperand),
|
||
// so -Inf -> -fMax. Only fpr-sourced operands (S/T) need this; ACC is written
|
||
// only by always-clamping acc-ops and can never be poisoned. In the default
|
||
// config (flag off) this emits nothing and returns the source reg.
|
||
static a64::VRegister fpuClampInput(const a64::VRegister& src, const a64::VRegister& scratch)
|
||
{
|
||
if (!CHECK_FPU_EXTRA_OVERFLOW)
|
||
return src;
|
||
armAsm->Fmov(scratch, src);
|
||
fpuClampCompareOperand(scratch);
|
||
return scratch;
|
||
}
|
||
|
||
// PS2 add/sub guard-bit emulation for the single-precision fast path.
|
||
//
|
||
// A compliant IEEE FPU keeps "guard" bits to the right of the mantissa during
|
||
// add/sub; the EE FPU does not. On a subtraction (or mixed-sign add) that
|
||
// left-shifts the mantissa, the bits that would have lived in those guard
|
||
// positions must read as zero on hardware. This masks the low mantissa bits of
|
||
// the smaller-exponent operand by the exponent difference, then does the single
|
||
// op. It is the arm64 fast-path port of x86 FPU_ADD_SUB (iFPU.cpp:402). Both
|
||
// JITs gate this masking on the same CHECK_FPU_GUARDED option (x86 FPU_ADD/
|
||
// FPU_SUB, iFPU.cpp) — ON by default (games like True Crime NYC and Jak 3
|
||
// misrender without it, and per-game flagging proved impractical) but opt-out
|
||
// globally for EE-heavy titles that don't need it; see the early-out below. It
|
||
// reproduces the masking already present in the DOUBLE path's FPU_ADD_SUB_D
|
||
// (iFPUd-arm64.cpp:200); eeClampMode 3 and up dispatch to that path instead and
|
||
// never reach here, and it guards unconditionally.
|
||
//
|
||
// When |expd - expt| <= 1 the mask clears zero bits, so that (common) case skips
|
||
// straight to the plain op. Only |diff| >= 2 masks the smaller-exponent operand;
|
||
// the guest fpr slots (EEREC_S/T/ACC) are never mutated, and s/t may alias dst.
|
||
// result = issub ? (s - t) : (s + t), written to dst.
|
||
//
|
||
// The masking is emitted one of two equivalent ways, chosen at build time by
|
||
// FPU_GUARD_MASK_STUB (iR5900-arm64.h): inlined here, or via a bl to the shared
|
||
// g_fpuGuardMaskStub. Both produce identical results.
|
||
//
|
||
// ⚠️ GPR scratch contract: this sequence touches only w0/w1 (RWARG1/2 — the
|
||
// raw-clobber habit the ARM64TYPE_FPRC pool exclusion in _allocArm64GPR is
|
||
// built around), w8 (RWSCRATCH), the non-allocatable x9/x10 value scratches
|
||
// and x16/x17 (vixl/addr scratch, via any temp-alloc eviction). None are in
|
||
// an int-allocator pool. It must NEVER touch x2-x7/x14/x15: a resident FCR31
|
||
// (GE-12, dirty from a C.cond/DIV/CTC1 earlier in the block) lives in that
|
||
// pool and survives across ops — clobbering it flips later BC1x/CFC1/flag
|
||
// writebacks. This is
|
||
// exactly the SotC geometry/movement bug (w2/w3 were used here); pinned by
|
||
// EeRecFpu.CompareSurvivesInterposedGuardedAdd{Cfc1,Bc1}.
|
||
static void fpuEmitGuardedAddSub(const a64::VRegister& dst,
|
||
const a64::VRegister& s, const a64::VRegister& t, bool issub)
|
||
{
|
||
// Guard-bit emulation is ON by default (CHECK_FPU_GUARDED) but can be turned
|
||
// off globally via the fpuGuardedAddSub Recompiler INI bool for EE-FPU-heavy
|
||
// titles verified to render fine without it. Off = a plain single op, matching
|
||
// AetherSX2 / PCSX2 v1.0 and the x86 FPU_ADD/FPU_SUB guard-off branch
|
||
// (iFPU.cpp). Returns before the NEON-temp alloc and GPR-scratch use below so
|
||
// nothing is booked on the fast path. (Full clamp mode is unaffected either
|
||
// way: it runs the DOUBLE path, which masks guard bits itself — iFPUd-arm64.cpp.)
|
||
if (!CHECK_FPU_GUARDED)
|
||
{
|
||
if (issub)
|
||
armAsm->Fsub(dst, s, t);
|
||
else
|
||
armAsm->Fadd(dst, s, t);
|
||
return;
|
||
}
|
||
|
||
// Alloc the NEON temp FIRST, before any raw GPR scratch below goes live.
|
||
// The alloc can emit a victim eviction whose address materialization uses
|
||
// scratch (today only x16/x17 via armMoveAddressToReg); keeping w9/w10
|
||
// dead across it is the defensive invariant, so a future eviction path
|
||
// that reaches for a value scratch can't corrupt our diff/mask. (EEREC_S/
|
||
// T/D/ACC are `needed` for the whole op and RSSCRATCH/RSSCRATCH2 are
|
||
// outside the pool, so the temp can never alias s/t/dst.)
|
||
const int tmp = _allocTempNEONreg();
|
||
const a64::VRegister vtmp = armSRegister(tmp);
|
||
|
||
const a64::Register rdiff = a64::w9; // non-allocatable scratch (iCore mask)
|
||
const a64::Register rmask = a64::w10; // non-allocatable scratch (iCore mask)
|
||
|
||
armAsm->Fmov(RWARG1, s); // s bits (non-destructive read)
|
||
armAsm->Fmov(RWARG2, t); // t bits
|
||
armAsm->Ubfx(rdiff, RWARG1, 23, 8); // expd
|
||
armAsm->Ubfx(RWSCRATCH, RWARG2, 23, 8); // expt
|
||
armAsm->Sub(rdiff, rdiff, RWSCRATCH); // diff = expd - expt (signed)
|
||
|
||
#if FPU_GUARD_MASK_STUB
|
||
a64::Label slow, done;
|
||
armAsm->Cmp(rdiff, 1);
|
||
armAsm->B(&slow, a64::gt); // diff >= 2
|
||
armAsm->Cmn(rdiff, 1);
|
||
armAsm->B(&slow, a64::lt); // diff <= -2
|
||
if (issub) // -1 <= diff <= 1: no masking
|
||
armAsm->Fsub(dst, s, t);
|
||
else
|
||
armAsm->Fadd(dst, s, t);
|
||
armAsm->B(&done);
|
||
|
||
armAsm->Bind(&slow);
|
||
// RWARG1/RWARG2 still hold s/t bits; the stub masks them in place
|
||
// (in: w0=A, w1=B; out: w0=maskedA, w1=maskedB; clobbers w0/w1/w8/w9/
|
||
// w10/w16/w17 + x30 — nothing in the int-allocator pools).
|
||
armEmitCall(g_fpuGuardMaskStub);
|
||
armAsm->Fmov(dst, RWARG1);
|
||
armAsm->Fmov(vtmp, RWARG2);
|
||
if (issub)
|
||
armAsm->Fsub(dst, dst, vtmp);
|
||
else
|
||
armAsm->Fadd(dst, dst, vtmp);
|
||
armAsm->Bind(&done);
|
||
#else
|
||
a64::Label maskT, maskS, plain, done;
|
||
|
||
armAsm->Cmp(rdiff, 1);
|
||
armAsm->B(&maskT, a64::gt); // diff >= 2 -> t smaller, mask t
|
||
armAsm->Cmn(rdiff, 1);
|
||
armAsm->B(&maskS, a64::lt); // diff <= -2 -> s smaller, mask s
|
||
armAsm->B(&plain); // -1 <= diff <= 1 -> mask nothing
|
||
|
||
// diff >= 2: mask t's low (diff-1) bits; diff >= 25 keeps only t's sign.
|
||
armAsm->Bind(&maskT);
|
||
{
|
||
a64::Label big, apply;
|
||
armAsm->Cmp(rdiff, 25);
|
||
armAsm->B(&big, a64::ge);
|
||
armAsm->Sub(RWSCRATCH, rdiff, 1);
|
||
armAsm->Mov(rmask, 0xffffffff);
|
||
armAsm->Lsl(rmask, rmask, RWSCRATCH);
|
||
armAsm->And(RWARG2, RWARG2, rmask);
|
||
armAsm->B(&apply);
|
||
armAsm->Bind(&big);
|
||
armAsm->And(RWARG2, RWARG2, 0x80000000);
|
||
armAsm->Bind(&apply);
|
||
armAsm->Fmov(vtmp, RWARG2);
|
||
}
|
||
if (issub)
|
||
armAsm->Fsub(dst, s, vtmp);
|
||
else
|
||
armAsm->Fadd(dst, s, vtmp);
|
||
armAsm->B(&done);
|
||
|
||
// diff <= -2: mask s's low (-diff-1) bits; diff <= -25 keeps only s's sign.
|
||
armAsm->Bind(&maskS);
|
||
{
|
||
a64::Label big, apply;
|
||
armAsm->Cmn(rdiff, 25);
|
||
armAsm->B(&big, a64::le);
|
||
armAsm->Neg(RWSCRATCH, rdiff);
|
||
armAsm->Sub(RWSCRATCH, RWSCRATCH, 1);
|
||
armAsm->Mov(rmask, 0xffffffff);
|
||
armAsm->Lsl(rmask, rmask, RWSCRATCH);
|
||
armAsm->And(RWARG1, RWARG1, rmask);
|
||
armAsm->B(&apply);
|
||
armAsm->Bind(&big);
|
||
armAsm->And(RWARG1, RWARG1, 0x80000000);
|
||
armAsm->Bind(&apply);
|
||
armAsm->Fmov(vtmp, RWARG1);
|
||
}
|
||
if (issub)
|
||
armAsm->Fsub(dst, vtmp, t);
|
||
else
|
||
armAsm->Fadd(dst, vtmp, t);
|
||
armAsm->B(&done);
|
||
|
||
armAsm->Bind(&plain);
|
||
if (issub)
|
||
armAsm->Fsub(dst, s, t);
|
||
else
|
||
armAsm->Fadd(dst, s, t);
|
||
|
||
armAsm->Bind(&done);
|
||
#endif
|
||
_freeNEONreg(tmp);
|
||
}
|
||
|
||
// The EE multiplier's one-ULP deficit is not modelled here: it needs the exact
|
||
// product's tail below the single's ULP, which a single-precision multiply has
|
||
// already discarded. It lives at emitDefectiveFmul (iFPUd-arm64.cpp), where the
|
||
// product is a double and the tail is 29 bits of it.
|
||
static void emitFpuMul(const a64::VRegister& dst, const a64::VRegister& s, const a64::VRegister& t)
|
||
{
|
||
armAsm->Fmul(dst, s, t);
|
||
}
|
||
|
||
//------------------------------------------------------------------
|
||
// Simple FPU ops — no clamping needed
|
||
//------------------------------------------------------------------
|
||
|
||
// "Full" / DOUBLE-precision emitters (iFPUd-arm64.cpp), selected per-op when
|
||
// CHECK_FPU_FULL (GameDB eeClampMode 3 and up). Default config uses the fast
|
||
// paths.
|
||
// MOV.S is a raw bit-copy in BOTH modes (x86 DOUBLE::recMOV_S_xmm == the fast
|
||
// body), so it has no DOUBLE selection.
|
||
namespace DOUBLE {
|
||
void recADD_S_xmm(int info);
|
||
void recSUB_S_xmm(int info);
|
||
void recADDA_S_xmm(int info);
|
||
void recSUBA_S_xmm(int info);
|
||
void recMUL_S_xmm(int info);
|
||
void recMULA_S_xmm(int info);
|
||
void recMADD_S_xmm(int info);
|
||
void recMSUB_S_xmm(int info);
|
||
void recMADDA_S_xmm(int info);
|
||
void recMSUBA_S_xmm(int info);
|
||
// GE-20: non-arith DOUBLE bodies.
|
||
void recABS_S_xmm(int info);
|
||
void recNEG_S_xmm(int info);
|
||
void recMAX_S_xmm(int info);
|
||
void recMIN_S_xmm(int info);
|
||
void recC_EQ_xmm(int info);
|
||
void recC_LT_xmm(int info);
|
||
void recC_LE_xmm(int info);
|
||
void recDIV_S_xmm(int info);
|
||
void recSQRT_S_xmm(int info);
|
||
void recRSQRT_S_xmm(int info);
|
||
} // namespace DOUBLE
|
||
|
||
static void recMOV_S_xmm(int info)
|
||
{
|
||
// MOV.S is a raw bit-copy (PS2 FPR[fd] = FPR[fs]); no clamp/NaN logic.
|
||
// Skip the emit entirely when fd and fs alias the same host reg (guest
|
||
// fs==fd): the allocator hands back EEREC_D==EEREC_S and the Fmov would be
|
||
// an identity self-move.
|
||
if (EEREC_D != EEREC_S)
|
||
armAsm->Fmov(armEeFprSlotReg(EEREC_D), armEeFprSlotReg(EEREC_S));
|
||
}
|
||
|
||
void recMOV_S()
|
||
{
|
||
eeFPURecompileCode(recMOV_S_xmm, Interp::MOV_S,
|
||
XMMINFO_WRITED | XMMINFO_READS);
|
||
}
|
||
|
||
// ABS.S / NEG.S are raw sign-bit operations on the EE — the interpreter does
|
||
// `& 0x7fffffff` / `^ 0x80000000` and the console agrees on every operand,
|
||
// including exponent-255 patterns and denormals. Neither clamps.
|
||
//
|
||
// The fast path used to clamp both to ±fMax, which corrupted 22 of the 54
|
||
// ABS/NEG operands in the first-party capture, in two ways:
|
||
//
|
||
// * exponent-255 in, +fMax out. Those are ordinary large PS2 floats, not
|
||
// infinities — abs(0x7F800000) is 0x7F800000, not 0x7F7FFFFF.
|
||
// * denormal in, ZERO out, on ABS only. The clamp was an Fminnm, an
|
||
// ARITHMETIC op, so FPCR.FZ flushed the operand before the compare even
|
||
// happened. NEG's clamp was an integer Smin/Umin and so never did this,
|
||
// which is exactly why only ABS lost its denormals.
|
||
//
|
||
// Fabs and Fneg alone are correct and total. They are non-arithmetic bit
|
||
// operations on AArch64 — no exceptions, no flush, NaN payloads through with
|
||
// only the sign changed — so they match x86's AND/XOR-with-mask and the
|
||
// interpreter's bit ops exactly. Same code the FULL path has always emitted
|
||
// (DOUBLE::recABS_S_xmm / recNEG_S_xmm, iFPUd-arm64.cpp).
|
||
static void recABS_S_xmm(int info)
|
||
{
|
||
fpuClearOUFlags();
|
||
armAsm->Fabs(armSRegister(EEREC_D), armSRegister(EEREC_S));
|
||
}
|
||
|
||
void recABS_S()
|
||
{
|
||
eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recABS_S_xmm : recABS_S_xmm, Interp::ABS_S,
|
||
XMMINFO_WRITED | XMMINFO_READS);
|
||
}
|
||
|
||
// See recABS_S_xmm above. The clamp deleted from here was the sign-preserving
|
||
// integer one (mirroring x86's switch from ClampValues to fpuFloat3, upstream
|
||
// 4ffbe0bbf) — which fixed NEG.S folding -NaN to +fMax, but kept the underlying
|
||
// mistake of clamping at all. The console does not.
|
||
static void recNEG_S_xmm(int info)
|
||
{
|
||
fpuClearOUFlags();
|
||
armAsm->Fneg(armSRegister(EEREC_D), armSRegister(EEREC_S));
|
||
}
|
||
|
||
void recNEG_S()
|
||
{
|
||
eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recNEG_S_xmm : recNEG_S_xmm, Interp::NEG_S,
|
||
XMMINFO_WRITED | XMMINFO_READS);
|
||
}
|
||
|
||
//------------------------------------------------------------------
|
||
// FPU Comparisons — set/clear fprc[31] condition bit
|
||
//------------------------------------------------------------------
|
||
|
||
void recC_F()
|
||
{
|
||
// Always false — clear condition bit
|
||
const int fl = fpuTryAllocFCR31(MODE_READ | MODE_WRITE);
|
||
if (fl >= 0)
|
||
{
|
||
armAsm->And(armWRegister(fl), armWRegister(fl), ~u32(FPUflagC));
|
||
}
|
||
else
|
||
{
|
||
armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||
armAsm->And(RWSCRATCH, RWSCRATCH, ~u32(FPUflagC));
|
||
armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||
}
|
||
}
|
||
|
||
// Shared C.EQ/C.LT/C.LE body (GE-12). Operand reads probe the NEON allocator
|
||
// read-only — a MODE_WRITE-only slot is authoritative, same rule as recMFC1 —
|
||
// with the clamped compare copies always going to scratch (the old shape
|
||
// flush-and-FREED both operands and reloaded them from memory). The flag
|
||
// write is a single Bfi of the Cset bit into the resident FCR31, replacing
|
||
// the Ldr + Mov/Bic/Orr(LSL 23) + Str chain; the Cset result parks in
|
||
// RWSCRATCH (w8), not w0 — pool regs can hold live allocator values.
|
||
static void recCompareFPRs(a64::Condition cond)
|
||
{
|
||
// Allocate FIRST: the alloc can emit an eviction store, which must not
|
||
// land between the Fcmp and the Cset (NZCV) nor split the scratch loads.
|
||
const int fl = fpuTryAllocFCR31(MODE_READ | MODE_WRITE);
|
||
|
||
const int fsreg = _checkNEONreg(NEONTYPE_FPREG, _Fs_, 0);
|
||
if (fsreg >= 0)
|
||
armAsm->Fmov(RSSCRATCH, armSRegister(fsreg));
|
||
else
|
||
armLoadEERegPtr(RSSCRATCH, &fpuRegs.fpr[_Fs_]);
|
||
const int ftreg = (_Ft_ == _Fs_) ? fsreg : _checkNEONreg(NEONTYPE_FPREG, _Ft_, 0);
|
||
if (ftreg >= 0)
|
||
armAsm->Fmov(RSSCRATCH2, armSRegister(ftreg));
|
||
else
|
||
armLoadEERegPtr(RSSCRATCH2, &fpuRegs.fpr[_Ft_]);
|
||
fpuClampCompareOperand(RSSCRATCH);
|
||
fpuClampCompareOperand(RSSCRATCH2);
|
||
armAsm->Fcmp(RSSCRATCH, RSSCRATCH2);
|
||
armAsm->Cset(RWSCRATCH, cond);
|
||
|
||
static_assert(FPUflagC == (1u << 23), "Bfi below inserts the C bit at bit 23");
|
||
if (fl >= 0)
|
||
{
|
||
armAsm->Bfi(armWRegister(fl), RWSCRATCH, 23, 1);
|
||
}
|
||
else
|
||
{
|
||
armLoadEERegPtr(RWARG1, &fpuRegs.fprc[31]);
|
||
armAsm->Bfi(RWARG1, RWSCRATCH, 23, 1);
|
||
armStoreEERegPtr(RWARG1, &fpuRegs.fprc[31]);
|
||
}
|
||
}
|
||
|
||
// GE-20: FULL mode compares as PS2-widened doubles with no operand clamping
|
||
// (DOUBLE::recC_*_xmm); the fast path clamps and compares as singles.
|
||
void recC_EQ()
|
||
{
|
||
if (CHECK_FPU_FULL)
|
||
{
|
||
eeFPURecompileCode(DOUBLE::recC_EQ_xmm, Interp::C_EQ, XMMINFO_READS | XMMINFO_READT);
|
||
return;
|
||
}
|
||
recCompareFPRs(a64::eq);
|
||
}
|
||
void recC_LT()
|
||
{
|
||
if (CHECK_FPU_FULL)
|
||
{
|
||
eeFPURecompileCode(DOUBLE::recC_LT_xmm, Interp::C_LT, XMMINFO_READS | XMMINFO_READT);
|
||
return;
|
||
}
|
||
recCompareFPRs(a64::lt);
|
||
}
|
||
void recC_LE()
|
||
{
|
||
if (CHECK_FPU_FULL)
|
||
{
|
||
eeFPURecompileCode(DOUBLE::recC_LE_xmm, Interp::C_LE, XMMINFO_READS | XMMINFO_READT);
|
||
return;
|
||
}
|
||
recCompareFPRs(a64::le);
|
||
}
|
||
|
||
//------------------------------------------------------------------
|
||
// FPU Arithmetic — native with PS2 clamping (no inf/nan)
|
||
//------------------------------------------------------------------
|
||
|
||
static void recADD_S_xmm(int info)
|
||
{
|
||
fpuClearOUFlags();
|
||
const a64::VRegister s = fpuClampInput(armSRegister(EEREC_S), RSSCRATCH);
|
||
const a64::VRegister t = fpuClampInput(armSRegister(EEREC_T), RSSCRATCH2);
|
||
fpuEmitGuardedAddSub(armSRegister(EEREC_D), s, t, false);
|
||
fpuClampResult(armSRegister(EEREC_D));
|
||
}
|
||
|
||
void recADD_S()
|
||
{
|
||
eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recADD_S_xmm : recADD_S_xmm, Interp::ADD_S,
|
||
XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT);
|
||
}
|
||
|
||
static void recSUB_S_xmm(int info)
|
||
{
|
||
fpuClearOUFlags();
|
||
const a64::VRegister s = fpuClampInput(armSRegister(EEREC_S), RSSCRATCH);
|
||
const a64::VRegister t = fpuClampInput(armSRegister(EEREC_T), RSSCRATCH2);
|
||
fpuEmitGuardedAddSub(armSRegister(EEREC_D), s, t, true);
|
||
fpuClampResult(armSRegister(EEREC_D));
|
||
}
|
||
|
||
void recSUB_S()
|
||
{
|
||
eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recSUB_S_xmm : recSUB_S_xmm, Interp::SUB_S,
|
||
XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT);
|
||
}
|
||
|
||
static void recMUL_S_xmm(int info)
|
||
{
|
||
fpuClearOUFlags();
|
||
const a64::VRegister s = fpuClampInput(armSRegister(EEREC_S), RSSCRATCH);
|
||
const a64::VRegister t = fpuClampInput(armSRegister(EEREC_T), RSSCRATCH2);
|
||
emitFpuMul(armSRegister(EEREC_D), s, t);
|
||
fpuClampResult(armSRegister(EEREC_D));
|
||
}
|
||
|
||
void recMUL_S()
|
||
{
|
||
eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recMUL_S_xmm : recMUL_S_xmm, Interp::MUL_S,
|
||
XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT);
|
||
}
|
||
|
||
// Emit: mov x9, #bitmask; msr FPCR, x9 — set FPCR to a compile-time-known
|
||
// bitmask. The PS2 FPU divides/sqrts in round-to-nearest while ADD/MUL round
|
||
// toward zero, so DIV must briefly swap FPCR to FPUDivFPCR and restore FPUFPCR
|
||
// after (mirrors x86 recDIV_S_xmm's xLDMXCSR pair). GE-13: the swap gate
|
||
// itself is baked per-compile (a CPU-config change resets the recompilers), so
|
||
// the VALUE is equally bake-safe — the old adrp+Ldr+Msr paid an address setup
|
||
// plus a dependent load on both sides of every DIV/SQRT.
|
||
static void emitLoadFPCR(u64 bitmask)
|
||
{
|
||
armAsm->Mov(a64::x9, bitmask);
|
||
armAsm->Msr(a64::FPCR, a64::x9);
|
||
}
|
||
|
||
// Native DIV.S — port of x86 recDIVhelper1 (CHECK_FPU_EXTRA_FLAGS is always on)
|
||
// + the recDIV_S_xmm FPCR round-mode swap. Matches interp DIV_S /
|
||
// checkDivideByZero:
|
||
// - divisor == 0 (exp field 0; FZ in FPCR flushes denormals so the float
|
||
// compare catches them too): result = sign(Fs^Ft) | 0x7f7fffff (±fMax),
|
||
// and set I|SI for 0/0, D|SD for x/0;
|
||
// - otherwise native Fdiv (round-to-nearest) then ±fMax result clamp.
|
||
// I|D are cleared first to match the divide-by-zero result-shape and sticky
|
||
// flag semantics of the interpreter.
|
||
static void recDIV_S_xmm(int info)
|
||
{
|
||
const bool swapFpcr = EmuConfig.Cpu.FPUFPCR.bitmask != EmuConfig.Cpu.FPUDivFPCR.bitmask;
|
||
if (swapFpcr)
|
||
emitLoadFPCR(EmuConfig.Cpu.FPUDivFPCR.bitmask);
|
||
|
||
// GE-13: no operand copies — every read of the raw (pre-clamp) fs/ft bits
|
||
// on the zero-divisor paths happens BEFORE the single EEREC_D write, so D
|
||
// aliasing S or T is safe; the normal path routes through fpuClampInput
|
||
// (scratch copies only when CHECK_FPU_EXTRA_OVERFLOW clamping is on, like
|
||
// the rest of the arithmetic family). The old shape paid two temp allocs
|
||
// plus two Fmovs unconditionally.
|
||
const a64::VRegister fs = armSRegister(EEREC_S);
|
||
const a64::VRegister ft = armSRegister(EEREC_T);
|
||
|
||
// GE-12: the three flag RMWs below (clear I|D, then I|SI or D|SD on the
|
||
// zero-divisor paths) hit the resident FCR31 — no Ldr/Str round-trips.
|
||
// The alloc emits any eviction store HERE, before the Fcmp chain (NZCV)
|
||
// and the branch arms (allocator calls inside a runtime-conditional emit
|
||
// region are forbidden — the Twinsanity class). GE-20 gave DIV a DOUBLE::
|
||
// variant, so this body no longer runs under FULL mode; the fl<0 fallback
|
||
// stays as defensive coverage.
|
||
const int fl = fpuTryAllocFCR31(MODE_READ | MODE_WRITE);
|
||
|
||
// Clear I|D.
|
||
if (fl >= 0)
|
||
armAsm->Bic(armWRegister(fl), armWRegister(fl), FPUflagI | FPUflagD);
|
||
else
|
||
{
|
||
armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||
armAsm->Bic(RWSCRATCH, RWSCRATCH, FPUflagI | FPUflagD);
|
||
armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||
}
|
||
|
||
a64::Label normal, setMax, xDiv, end;
|
||
|
||
armAsm->Fcmp(ft, 0.0);
|
||
armAsm->B(&normal, a64::ne); // divisor != 0 → normal divide (unordered too)
|
||
|
||
// Divisor is zero: distinguish 0/0 (I|SI) from x/0 (D|SD).
|
||
armAsm->Fcmp(fs, 0.0);
|
||
armAsm->B(&xDiv, a64::ne);
|
||
if (fl >= 0)
|
||
armAsm->Orr(armWRegister(fl), armWRegister(fl), FPUflagI | FPUflagSI);
|
||
else
|
||
{
|
||
armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||
armAsm->Orr(RWSCRATCH, RWSCRATCH, FPUflagI | FPUflagSI);
|
||
armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||
}
|
||
armAsm->B(&setMax);
|
||
armAsm->Bind(&xDiv);
|
||
if (fl >= 0)
|
||
armAsm->Orr(armWRegister(fl), armWRegister(fl), FPUflagD | FPUflagSD);
|
||
else
|
||
{
|
||
armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||
armAsm->Orr(RWSCRATCH, RWSCRATCH, FPUflagD | FPUflagSD);
|
||
armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||
}
|
||
|
||
armAsm->Bind(&setMax);
|
||
// result = sign(Fs ^ Ft) | 0x7f7fffff — raw sign reads precede the D write.
|
||
armAsm->Fmov(RWARG1, fs);
|
||
armAsm->Fmov(RWARG2, ft);
|
||
armAsm->Eor(RWARG1, RWARG1, RWARG2);
|
||
armAsm->And(RWARG1, RWARG1, 0x80000000);
|
||
armAsm->Orr(RWARG1, RWARG1, 0x7f7fffff);
|
||
armAsm->Fmov(armSRegister(EEREC_D), RWARG1);
|
||
armAsm->B(&end);
|
||
|
||
armAsm->Bind(&normal);
|
||
{
|
||
const a64::VRegister s = fpuClampInput(fs, RSSCRATCH);
|
||
const a64::VRegister t = fpuClampInput(ft, RSSCRATCH2);
|
||
armAsm->Fdiv(armSRegister(EEREC_D), s, t);
|
||
fpuClampResult(armSRegister(EEREC_D));
|
||
}
|
||
|
||
armAsm->Bind(&end);
|
||
|
||
if (swapFpcr)
|
||
emitLoadFPCR(EmuConfig.Cpu.FPUFPCR.bitmask);
|
||
}
|
||
|
||
void recDIV_S()
|
||
{
|
||
eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recDIV_S_xmm : recDIV_S_xmm, Interp::DIV_S,
|
||
XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT);
|
||
}
|
||
|
||
static void recSQRT_S_xmm(int info)
|
||
{
|
||
const a64::VRegister ft = armSRegister(EEREC_T);
|
||
|
||
// PS2 SQRT.S rounds to nearest regardless of the configured FCR31 rounding
|
||
// mode (same hardware quirk as DIV.S — see recDIV_S_xmm + the emitLoadFPCR
|
||
// comment). The EE rec runs under host FPCR = FPUFPCR (ChopZero by default),
|
||
// so swap to the nearest-rounding FPUDivFPCR around the Fsqrt and restore
|
||
// FPUFPCR after. Mirrors x86 recSQRT_S_xmm (iFPU.cpp:1745-1782).
|
||
const bool swapFpcr = EmuConfig.Cpu.FPUFPCR.bitmask != EmuConfig.Cpu.FPUDivFPCR.bitmask;
|
||
if (swapFpcr)
|
||
emitLoadFPCR(EmuConfig.Cpu.FPUDivFPCR.bitmask);
|
||
|
||
// PS2 SQRT.S flag handling (interp SQRT_S, FPU.cpp; CHECK_FPU_EXTRA_FLAGS
|
||
// is always on): clear I|D unconditionally, then set I|SI whenever Ft's
|
||
// sign bit is set. The exponent field plays no part — -0 and the negative
|
||
// denormals raise I|SI too. x86's recSQRT_S_xmm tests MOVMSKPS & 1 the same
|
||
// way (iFPU.cpp), as does the FULL-mode DOUBLE path (iFPUd-arm64.cpp). See
|
||
// EeRecFpu.SqrtSInvalidFlagFollowsTheSignBitAlone.
|
||
// Read the Ft bits before Fabs clobbers EEREC_D, which may alias EEREC_T.
|
||
// GE-12: flag RMW on the resident FCR31; alloc first (eviction stores
|
||
// must precede the RWARG1 clobber and the branch arms). GE-20 gave SQRT
|
||
// a DOUBLE:: variant, so this body no longer runs under FULL mode; the
|
||
// fl<0 fallback stays as defensive coverage.
|
||
const int fl = fpuTryAllocFCR31(MODE_READ | MODE_WRITE);
|
||
armAsm->Fmov(RWARG1, ft);
|
||
const a64::Register flagReg = (fl >= 0) ? armWRegister(fl) : RWSCRATCH;
|
||
if (fl < 0)
|
||
armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||
armAsm->Bic(flagReg, flagReg, FPUflagI | FPUflagD);
|
||
a64::Label skipFlag;
|
||
armAsm->Tbz(RWARG1, 31, &skipFlag); // positive → no flag
|
||
armAsm->Orr(flagReg, flagReg, FPUflagI | FPUflagSI);
|
||
armAsm->Bind(&skipFlag);
|
||
if (fl < 0)
|
||
armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||
|
||
// PS2 takes sqrt of |ft| → Fabs first.
|
||
armAsm->Fabs(armSRegister(EEREC_D), ft);
|
||
|
||
// Exponent-255 operands. On the EE that is an ORDINARY binade — no Inf, no
|
||
// NaN, and the representable max is 0x7FFFFFFF, not FLT_MAX — but the host
|
||
// reads those patterns as Inf/NaN, so a plain Fsqrt returns Inf/NaN and
|
||
// fpuClampResult flattens it to +fMax. This used to be papered over with a
|
||
// source-operand clamp down to +fMax (an integer Umin, gated on
|
||
// CHECK_FPU_OVERFLOW, mirroring x86's `xMIN.SS(EEREC_D, g_maxvals[0])` at
|
||
// iFPU.cpp:1777, itself mirroring the interpreter's fpuDouble). That put
|
||
// interp, x86 and arm64 on ONE answer, which is a weaker property than the
|
||
// RIGHT answer: all three landed two binades below the console —
|
||
// sqrt(0x7F800000) = 0x5F7FFFFF against silicon's 0x5F800000, and
|
||
// sqrt(0x7FFFFFFF) = 0x5F7FFFFF against 0x5FB504F3.
|
||
//
|
||
// Instead, square-root |Ft|/4 and double it. sqrt halves exponents, so the
|
||
// scaled operand (exponent field 253) and the doubled result are both
|
||
// ordinary singles — this needs no wider format and so stays on the fast
|
||
// path, which is single-precision by design. 4 is an even power of two, so
|
||
// its own square root is exact and the identity contributes no rounding of
|
||
// its own; the Fsqrt is the only rounding step, and Fadd(d,d,d) doubles
|
||
// exactly in any rounding mode. It is the same power-of-two prescale
|
||
// ToDouble() uses to carry these operands into FULL mode (iFPUd-arm64.cpp),
|
||
// with the factor picked to suit sqrt.
|
||
//
|
||
// Ungated on purpose. The old clamp fired only on exponent-255 patterns
|
||
// (over non-negative floats the unsigned integer order IS the IEEE order,
|
||
// so everything at or below 0x7F7FFFFF passed through untouched), and with
|
||
// it off the same operands came out at 0x7F7FFFFF instead — wrong in every
|
||
// clamp mode, just differently wrong. So this branch replaces it in all of
|
||
// them and moves nothing else: exponent <= 254 still takes the plain Fsqrt.
|
||
//
|
||
// It also sidesteps the host NaN taxonomy entirely, which is what made the
|
||
// clamp delicate: Fminnm is not MINSS (MINSS returns src2 for ANY NaN,
|
||
// FMINNM only prefers the number against a QUIET one), so the clamp had to
|
||
// be an integer Umin to cover the 4194303 signalling patterns that are just
|
||
// ordinary large floats to the EE. Testing the exponent field, as the
|
||
// interpreter's fpuDouble does, never asks the question.
|
||
// Pinned by EeFpuOverflowConsole.SqrtMatchesConsoleOnEveryExponent255Operand.
|
||
{
|
||
const a64::VRegister d = armSRegister(EEREC_D);
|
||
a64::Label ordinary, sqrtDone;
|
||
|
||
armAsm->Fmov(RWSCRATCH, d);
|
||
armAsm->And(RWARG1, RWSCRATCH, 0x7f800000);
|
||
armAsm->Cmp(RWARG1, 0x7f800000);
|
||
armAsm->B(&ordinary, a64::ne);
|
||
|
||
// |Ft| / 4: lower the exponent field by two. 0x01000000 is not an
|
||
// add/sub immediate (0x1000 needs 13 bits), so it goes in two steps.
|
||
armAsm->Sub(RWSCRATCH, RWSCRATCH, 0x00800000);
|
||
armAsm->Sub(RWSCRATCH, RWSCRATCH, 0x00800000);
|
||
armAsm->Fmov(d, RWSCRATCH);
|
||
armAsm->Fsqrt(d, d);
|
||
armAsm->Fadd(d, d, d); // *2, exact
|
||
armAsm->B(&sqrtDone);
|
||
|
||
armAsm->Bind(&ordinary);
|
||
armAsm->Fsqrt(d, d);
|
||
|
||
armAsm->Bind(&sqrtDone);
|
||
fpuClampResult(d);
|
||
}
|
||
|
||
if (swapFpcr)
|
||
emitLoadFPCR(EmuConfig.Cpu.FPUFPCR.bitmask);
|
||
}
|
||
|
||
void recSQRT_S()
|
||
{
|
||
eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recSQRT_S_xmm : recSQRT_S_xmm, Interp::SQRT_S,
|
||
XMMINFO_WRITED | XMMINFO_READT);
|
||
}
|
||
|
||
// Native RSQRT.S: Fd = Fs / sqrt(|Ft|) implemented in the shape of
|
||
// recDIV_S_xmm/recSQRT_S_xmm and matching interp RSQRT_S (FPU.cpp) and
|
||
// x86 recRSQRThelper1 (CHECK_FPU_EXTRA_FLAGS always on):
|
||
// - Ft exponent field == 0 (zero, including denormals-as-zero): result is
|
||
// sign(Ft) | 0x7f7fffff (+/-fMax), and set D|SD;
|
||
// - Ft negative (exp nonzero): set I|SI, then divide by sqrt(|Ft|);
|
||
// - Ft positive nonzero: divide by sqrt(Ft).
|
||
// I|D are cleared first (sticky SI|SD survive). Like DIV.S/SQRT.S the PS2
|
||
// rounds RSQRT to nearest regardless of the configured FCR31 rounding mode, so
|
||
// swap host FPCR to the nearest-rounding FPUDivFPCR around the sqrt+div and
|
||
// restore FPUFPCR after
|
||
static void recRSQRT_S_xmm(int info)
|
||
{
|
||
const bool swapFpcr = EmuConfig.Cpu.FPUFPCR.bitmask != EmuConfig.Cpu.FPUDivFPCR.bitmask;
|
||
if (swapFpcr)
|
||
emitLoadFPCR(EmuConfig.Cpu.FPUDivFPCR.bitmask);
|
||
|
||
// Copy operands into temps: EEREC_D may alias EEREC_S/EEREC_T, and the
|
||
// zero-divisor path needs the raw Ft sign bit after EEREC_D is written.
|
||
const int dreg = _allocTempNEONreg(); // dividend Fs
|
||
const int treg = _allocTempNEONreg(); // divisor, made |Ft| for the sqrt
|
||
armAsm->Fmov(armSRegister(dreg), armSRegister(EEREC_S));
|
||
armAsm->Fmov(armSRegister(treg), armSRegister(EEREC_T));
|
||
|
||
// GE-12: the three flag RMWs below go to the resident FCR31 when there is
|
||
// one. Alloc here, before the RWARG1 clobber and before the branch arms —
|
||
// any eviction store the alloc emits has to land outside a
|
||
// runtime-conditional emit region, same rule as recDIV_S_xmm/recSQRT_S_xmm.
|
||
const int fl = fpuTryAllocFCR31(MODE_READ | MODE_WRITE);
|
||
const a64::Register flagReg = (fl >= 0) ? armWRegister(fl) : RWSCRATCH;
|
||
|
||
// Raw Ft bits drive the zero/negative branch and the +/-fMax result sign.
|
||
armAsm->Fmov(RWARG1, armSRegister(EEREC_T));
|
||
|
||
a64::Label notZero, xOverZero, flagsDone, ftPositive, end;
|
||
|
||
// Clear I|D (sticky SI|SD are left intact), then I from the divisor's sign
|
||
// bit, before the zero test -- see RSQRT_S in FPU.cpp for why the order.
|
||
if (fl < 0)
|
||
armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||
armAsm->Bic(flagReg, flagReg, FPUflagI | FPUflagD);
|
||
armAsm->Tbz(RWARG1, 31, &ftPositive);
|
||
armAsm->Orr(flagReg, flagReg, FPUflagI | FPUflagSI);
|
||
armAsm->Bind(&ftPositive);
|
||
if (fl < 0)
|
||
armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||
|
||
// Ft is treated as zero when its exponent field is 0 (denormals included).
|
||
armAsm->Tst(RWARG1, 0x7F800000);
|
||
armAsm->B(¬Zero, a64::ne);
|
||
|
||
armAsm->Fmov(RWARG2, armSRegister(dreg)); // raw Fs bits, saved before any write
|
||
|
||
// The dividend decides the cause: 0/0 raises I|SI, x/0 raises D|SD. Same
|
||
// split as recDIV_S_xmm above and DOUBLE::recRSQRT_S_xmm in
|
||
// iFPUd-arm64.cpp. Tested on the exponent field, like the divisor above,
|
||
// so FPCR.FZ does not decide whether a denormal dividend counts as zero.
|
||
if (fl < 0)
|
||
armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||
armAsm->Tst(RWARG2, 0x7F800000);
|
||
armAsm->B(&xOverZero, a64::ne);
|
||
armAsm->Orr(flagReg, flagReg, FPUflagI | FPUflagSI); // 0/0
|
||
armAsm->B(&flagsDone);
|
||
armAsm->Bind(&xOverZero);
|
||
armAsm->Orr(flagReg, flagReg, FPUflagD | FPUflagSD); // x/0
|
||
armAsm->Bind(&flagsDone);
|
||
if (fl < 0)
|
||
armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||
|
||
// Result = sign(FS) | 0x7f7fffff.
|
||
//
|
||
// FS, not FT. This op divides by sqrt(|Ft|), so by the time the division
|
||
// happens the divisor has no sign left to contribute -- only the dividend
|
||
// does. This emitter used Ft's sign and was alone in doing so: x86
|
||
// recRSQRThelper1 (iFPU.cpp) takes Fs's, and so does the console.
|
||
// rsqrt(+0, -0) is the row that separates them, and it is in the capture --
|
||
// console +0x7FFFFFFF, upstream x86 JIT +0x7F7FFFFF, this emitter
|
||
// -0x7F7FFFFF. Pinned by EeRecFpuRsqrt.ZeroDivisorSignComesFromTheDividend.
|
||
//
|
||
// The MAGNITUDE stays at FLT_MAX rather than the console's 0x7FFFFFFF: this
|
||
// tier saturates in host singles throughout and cannot hold the EE's top
|
||
// binade. That is the standing fast-path compromise, not this fix.
|
||
armAsm->And(RWARG2, RWARG2, 0x80000000);
|
||
armAsm->Orr(RWARG2, RWARG2, 0x7f7fffff);
|
||
armAsm->Fmov(armSRegister(EEREC_D), RWARG2);
|
||
armAsm->B(&end);
|
||
|
||
armAsm->Bind(¬Zero);
|
||
armAsm->Fabs(armSRegister(treg), armSRegister(treg)); // |Ft| (no-op if positive)
|
||
if (CHECK_FPU_EXTRA_OVERFLOW)
|
||
{
|
||
fpuClampCompareOperand(armSRegister(dreg));
|
||
fpuClampCompareOperand(armSRegister(treg));
|
||
}
|
||
|
||
// Exponent-255 divisor: recSQRT_S_xmm's prescale, applied to the square
|
||
// root this op does inline. Rebuilt from the raw Ft word so the operand
|
||
// clamp above cannot get in front of it. The dividend keeps the fast
|
||
// path's saturation.
|
||
{
|
||
a64::Label ordinaryDivisor, sqrtDone;
|
||
armAsm->Ubfx(RWARG2, RWARG1, 23, 8);
|
||
armAsm->Cmp(RWARG2, 0xff);
|
||
armAsm->B(&ordinaryDivisor, a64::ne);
|
||
|
||
armAsm->And(RWARG2, RWARG1, 0x7fffffff); // |Ft|
|
||
armAsm->Sub(RWARG2, RWARG2, 0x00800000); // /4 — 0x01000000 is not an
|
||
armAsm->Sub(RWARG2, RWARG2, 0x00800000); // add/sub immediate
|
||
armAsm->Fmov(armSRegister(treg), RWARG2);
|
||
armAsm->Fsqrt(armSRegister(treg), armSRegister(treg));
|
||
armAsm->Fadd(armSRegister(treg), armSRegister(treg), armSRegister(treg));
|
||
armAsm->B(&sqrtDone);
|
||
|
||
armAsm->Bind(&ordinaryDivisor);
|
||
armAsm->Fsqrt(armSRegister(treg), armSRegister(treg));
|
||
|
||
armAsm->Bind(&sqrtDone);
|
||
}
|
||
|
||
armAsm->Fdiv(armSRegister(EEREC_D), armSRegister(dreg), armSRegister(treg));
|
||
fpuClampResult(armSRegister(EEREC_D));
|
||
|
||
armAsm->Bind(&end);
|
||
|
||
_freeNEONreg(dreg);
|
||
_freeNEONreg(treg);
|
||
|
||
if (swapFpcr)
|
||
emitLoadFPCR(EmuConfig.Cpu.FPUFPCR.bitmask);
|
||
}
|
||
|
||
void recRSQRT_S()
|
||
{
|
||
// GE-20: FULL mode gets the x86 DOUBLE body (widen -> sqrt+div in double,
|
||
// zero-divisor max keyed off the DIVIDEND's sign per x86).
|
||
if (CHECK_FPU_FULL)
|
||
{
|
||
eeFPURecompileCode(DOUBLE::recRSQRT_S_xmm, Interp::RSQRT_S,
|
||
XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT);
|
||
return;
|
||
}
|
||
eeFPURecompileCode(recRSQRT_S_xmm, Interp::RSQRT_S,
|
||
XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT);
|
||
}
|
||
|
||
// MAX.S / MIN.S are bit SELECTION, not arithmetic. The console orders the two
|
||
// raw words by (sign, magnitude) and writes the winner's word through
|
||
// unchanged — that is `fp_max`/`fp_min` in FPU.cpp, a pure integer compare, and
|
||
// it is exactly what the DOUBLE tier already does (DOUBLE::recMINMAX, the port
|
||
// of x86 iFPUd.cpp recMINMAX: "FPU's MAX/MIN work with all numbers (including
|
||
// denormals)"). Because there is no arithmetic in the op, the single/double
|
||
// split has nothing to say about it and both tiers owe the same answer.
|
||
//
|
||
// The fast path used to compute it with Fmaxnm/Fminnm over ±fMax-clamped
|
||
// operands (the x86 iFPU.cpp fast tier still does, via recCommutativeOp op>=2).
|
||
// Two whole operand classes come back wrong that way, and both are visible in
|
||
// the SCPH-90000 capture — 38 of 66 MAX cases and 16 of 66 MIN cases:
|
||
//
|
||
// denormals Fmaxnm/Fminnm are arithmetic, so FPCR.FZ flushes the operand
|
||
// and the winner's word is lost: max(0x00000001, 0) reads back
|
||
// 0x00000000 where the console says 0x00000001. (30 MAX, 14 MIN.)
|
||
// exponent255 the ±fMax operand clamp folds every top-binade word onto
|
||
// 0x7F7FFFFF: max(0x7F7FFFFF, 0x7FFFFFFF) reads back 0x7F7FFFFF
|
||
// where the console says 0x7FFFFFFF. Exponent 255 is an ordinary
|
||
// binade on the EE and 0x7FFFFFFF is the largest number there is
|
||
// — the same defect ABS.S/NEG.S carried until cbf04acba1.
|
||
// (8 MAX, 2 MIN.)
|
||
//
|
||
// Dropping the clamp does not reopen the True Crime: New York City rainbow that
|
||
// motivated it (a raw Inf/NaN operand being eaten by Fmaxnm and losing to the
|
||
// small operand): the integer ordering has no NaN concept to eat, so a
|
||
// pseudo-inf operand wins the max as the huge number the console treats it as.
|
||
//
|
||
// Ordering key: k(x) = x ^ ((x >>s 31) >>u 1) flips the low 31 bits of a
|
||
// negative word and leaves a positive one alone, so a SIGNED compare of the
|
||
// keys is the console's total order. It is an involution, but there is no need
|
||
// to invert it — Csel picks between the untouched originals.
|
||
//
|
||
// ⚠️ Same GPR scratch contract as fpuEmitGuardedAddSub: w0/w1/w8 and the
|
||
// non-allocatable w9 only. Never x2-x7/x14/x15, where a resident FCR31 lives.
|
||
// fpuClearOUFlags() runs first, so any allocator eviction it emits lands before
|
||
// the raw scratch goes live.
|
||
static void recMINMAX(int info, bool ismin)
|
||
{
|
||
fpuClearOUFlags();
|
||
|
||
const a64::Register sbits = RWARG1, tbits = RWARG2;
|
||
const a64::Register skey = RWSCRATCH, tkey = a64::w9;
|
||
|
||
armAsm->Fmov(sbits, armSRegister(EEREC_S));
|
||
armAsm->Fmov(tbits, armSRegister(EEREC_T));
|
||
armAsm->Asr(skey, sbits, 31);
|
||
armAsm->Eor(skey, sbits, a64::Operand(skey, a64::LSR, 1));
|
||
armAsm->Asr(tkey, tbits, 31);
|
||
armAsm->Eor(tkey, tbits, a64::Operand(tkey, a64::LSR, 1));
|
||
armAsm->Cmp(skey, tkey);
|
||
// Equal keys mean identical words, so either arm is correct there.
|
||
armAsm->Csel(sbits, sbits, tbits, ismin ? a64::le : a64::ge);
|
||
armAsm->Fmov(armSRegister(EEREC_D), sbits);
|
||
}
|
||
|
||
static void recMAX_S_xmm(int info) { recMINMAX(info, false); }
|
||
static void recMIN_S_xmm(int info) { recMINMAX(info, true); }
|
||
|
||
// FULL mode keeps dispatching to DOUBLE::recMINMAX rather than sharing this
|
||
// body. The two are semantically identical (both are fp_max/fp_min) and the
|
||
// capture scores the DOUBLE one 66/66 on both ops in all 1024 grid cells, so
|
||
// there is nothing to gain by moving it — and the tiers deliberately differ on
|
||
// FCR31 residency: fpuTryAllocFCR31 returns -1 under CHECK_FPU_FULL because the
|
||
// DOUBLE bodies RMW fprc[31] through memory (GE-12 comment at the top of this
|
||
// file). Keeping the split keeps that invariant local to each tier.
|
||
void recMAX_S()
|
||
{
|
||
eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recMAX_S_xmm : recMAX_S_xmm, Interp::MAX_S,
|
||
XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT);
|
||
}
|
||
|
||
void recMIN_S()
|
||
{
|
||
eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recMIN_S_xmm : recMIN_S_xmm, Interp::MIN_S,
|
||
XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT);
|
||
}
|
||
|
||
//------------------------------------------------------------------
|
||
// FPU Accumulator ops — ACC = fs OP ft, then fd = ACC OP fs2
|
||
//------------------------------------------------------------------
|
||
|
||
static void recADDA_S_xmm(int info)
|
||
{
|
||
fpuClearOUFlags();
|
||
const a64::VRegister s = fpuClampInput(armSRegister(EEREC_S), RSSCRATCH);
|
||
const a64::VRegister t = fpuClampInput(armSRegister(EEREC_T), RSSCRATCH2);
|
||
fpuEmitGuardedAddSub(armSRegister(EEREC_ACC), s, t, false);
|
||
fpuClampResult(armSRegister(EEREC_ACC));
|
||
}
|
||
|
||
void recADDA_S()
|
||
{
|
||
eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recADDA_S_xmm : recADDA_S_xmm, Interp::ADDA_S,
|
||
XMMINFO_WRITEACC | XMMINFO_READS | XMMINFO_READT);
|
||
}
|
||
|
||
static void recSUBA_S_xmm(int info)
|
||
{
|
||
fpuClearOUFlags();
|
||
const a64::VRegister s = fpuClampInput(armSRegister(EEREC_S), RSSCRATCH);
|
||
const a64::VRegister t = fpuClampInput(armSRegister(EEREC_T), RSSCRATCH2);
|
||
fpuEmitGuardedAddSub(armSRegister(EEREC_ACC), s, t, true);
|
||
fpuClampResult(armSRegister(EEREC_ACC));
|
||
}
|
||
|
||
void recSUBA_S()
|
||
{
|
||
eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recSUBA_S_xmm : recSUBA_S_xmm, Interp::SUBA_S,
|
||
XMMINFO_WRITEACC | XMMINFO_READS | XMMINFO_READT);
|
||
}
|
||
|
||
static void recMULA_S_xmm(int info)
|
||
{
|
||
fpuClearOUFlags();
|
||
const a64::VRegister s = fpuClampInput(armSRegister(EEREC_S), RSSCRATCH);
|
||
const a64::VRegister t = fpuClampInput(armSRegister(EEREC_T), RSSCRATCH2);
|
||
emitFpuMul(armSRegister(EEREC_ACC), s, t);
|
||
fpuClampResult(armSRegister(EEREC_ACC));
|
||
}
|
||
|
||
void recMULA_S()
|
||
{
|
||
eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recMULA_S_xmm : recMULA_S_xmm, Interp::MULA_S,
|
||
XMMINFO_WRITEACC | XMMINFO_READS | XMMINFO_READT);
|
||
}
|
||
|
||
// fd = ACC + fs * ft. PS2 ISA mandates two separate roundings (mul then
|
||
// add), so don't fuse into FMA. RSSCRATCH (s30) is the non-pool scratch
|
||
// for the intermediate product — leaves EEREC_S/T allocator-resident.
|
||
static void recMADD_S_xmm(int info)
|
||
{
|
||
fpuClearOUFlags();
|
||
const a64::VRegister s = fpuClampInput(armSRegister(EEREC_S), RSSCRATCH);
|
||
const a64::VRegister t = fpuClampInput(armSRegister(EEREC_T), RSSCRATCH2);
|
||
emitFpuMul(RSSCRATCH, s, t);
|
||
// Intermediate-product clamp gated on CHECK_FPU_EXTRA_OVERFLOW — x86-JIT
|
||
// parity (GE-19; x86 recMADDtemp applies fpuFloat to the product under
|
||
// the same gate). The INTERPRETER always clamps this product (fpuDouble
|
||
// temp, FPU.cpp:271-277), so on the product-overflow + opposite-sign-ACC
|
||
// corner default-mode JIT = ±fMax while interp = 0 — divergence BY
|
||
// DESIGN, shared with x86; games are tuned against the x86 JIT. Pinned
|
||
// by EeRecFpu.MaddSProductOverflowDefaultModeMatchesX86Jit.
|
||
// (x86 also pre-clamps ACC here under the gate; our ACC writers all end
|
||
// in an unconditional fpuClampResult, so a resident ACC can never hold
|
||
// Inf/NaN and that leg is dead on arm64.)
|
||
if (CHECK_FPU_EXTRA_OVERFLOW)
|
||
fpuClampResult(RSSCRATCH);
|
||
fpuEmitGuardedAddSub(armSRegister(EEREC_D), armSRegister(EEREC_ACC), RSSCRATCH, false);
|
||
fpuClampResult(armSRegister(EEREC_D));
|
||
}
|
||
|
||
void recMADD_S()
|
||
{
|
||
eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recMADD_S_xmm : recMADD_S_xmm, Interp::MADD_S,
|
||
XMMINFO_WRITED | XMMINFO_READACC | XMMINFO_READS | XMMINFO_READT);
|
||
}
|
||
|
||
// fd = ACC - fs * ft
|
||
static void recMSUB_S_xmm(int info)
|
||
{
|
||
fpuClearOUFlags();
|
||
const a64::VRegister s = fpuClampInput(armSRegister(EEREC_S), RSSCRATCH);
|
||
const a64::VRegister t = fpuClampInput(armSRegister(EEREC_T), RSSCRATCH2);
|
||
emitFpuMul(RSSCRATCH, s, t);
|
||
// Extra-gated product clamp — x86-JIT parity, see recMADD_S_xmm (GE-19).
|
||
if (CHECK_FPU_EXTRA_OVERFLOW)
|
||
fpuClampResult(RSSCRATCH);
|
||
fpuEmitGuardedAddSub(armSRegister(EEREC_D), armSRegister(EEREC_ACC), RSSCRATCH, true);
|
||
fpuClampResult(armSRegister(EEREC_D));
|
||
}
|
||
|
||
void recMSUB_S()
|
||
{
|
||
eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recMSUB_S_xmm : recMSUB_S_xmm, Interp::MSUB_S,
|
||
XMMINFO_WRITED | XMMINFO_READACC | XMMINFO_READS | XMMINFO_READT);
|
||
}
|
||
|
||
// ACC = ACC + fs * ft. In the default clamp mode the raw fs*ft product is
|
||
// added unclamped — interp MADDA_S (FPU.cpp) has no fpuDouble temp for the
|
||
// product, and x86 recMADDtemp doesn't clamp it either; both agree that an
|
||
// overflowing product overflows the accumulate (→ ±fMax) rather than
|
||
// cancelling an opposite-signed ACC. Under CHECK_FPU_EXTRA_OVERFLOW the x86
|
||
// JIT clamps the A-form product too (same recMADDtemp serves MADD and MADDA)
|
||
// — mirror it (GE-19; x86-JIT parity is the bar, and there interp diverges
|
||
// the other way by never clamping the A-form product).
|
||
static void recMADDA_S_xmm(int info)
|
||
{
|
||
fpuClearOUFlags();
|
||
const a64::VRegister s = fpuClampInput(armSRegister(EEREC_S), RSSCRATCH);
|
||
const a64::VRegister t = fpuClampInput(armSRegister(EEREC_T), RSSCRATCH2);
|
||
emitFpuMul(RSSCRATCH, s, t);
|
||
if (CHECK_FPU_EXTRA_OVERFLOW)
|
||
fpuClampResult(RSSCRATCH);
|
||
fpuEmitGuardedAddSub(armSRegister(EEREC_ACC), armSRegister(EEREC_ACC), RSSCRATCH, false);
|
||
fpuClampResult(armSRegister(EEREC_ACC));
|
||
}
|
||
|
||
void recMADDA_S()
|
||
{
|
||
eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recMADDA_S_xmm : recMADDA_S_xmm, Interp::MADDA_S,
|
||
XMMINFO_WRITEACC | XMMINFO_READACC | XMMINFO_READS | XMMINFO_READT);
|
||
}
|
||
|
||
// ACC = ACC - fs * ft. Same as MADDA_S: raw product in the default mode,
|
||
// extra-gated product clamp for x86-JIT parity (GE-19).
|
||
static void recMSUBA_S_xmm(int info)
|
||
{
|
||
fpuClearOUFlags();
|
||
const a64::VRegister s = fpuClampInput(armSRegister(EEREC_S), RSSCRATCH);
|
||
const a64::VRegister t = fpuClampInput(armSRegister(EEREC_T), RSSCRATCH2);
|
||
emitFpuMul(RSSCRATCH, s, t);
|
||
if (CHECK_FPU_EXTRA_OVERFLOW)
|
||
fpuClampResult(RSSCRATCH);
|
||
fpuEmitGuardedAddSub(armSRegister(EEREC_ACC), armSRegister(EEREC_ACC), RSSCRATCH, true);
|
||
fpuClampResult(armSRegister(EEREC_ACC));
|
||
}
|
||
|
||
void recMSUBA_S()
|
||
{
|
||
eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recMSUBA_S_xmm : recMSUBA_S_xmm, Interp::MSUBA_S,
|
||
XMMINFO_WRITEACC | XMMINFO_READACC | XMMINFO_READS | XMMINFO_READT);
|
||
}
|
||
|
||
// CVT.S: fd = (float)int_bits_of(fpr[fs])
|
||
// Single NEON-scalar SCVTF Sd,Sn — the int32 bits are already in the V file;
|
||
// the old Fmov-to-GPR bounce cost an extra insn + cross-file hazard (GE-02).
|
||
// A relocated slot puts them back out of reach, so the double tier pays the
|
||
// bounce.
|
||
static void recCVT_S_xmm(int info)
|
||
{
|
||
if (!CHECK_FPU_FULL)
|
||
{
|
||
armAsm->Scvtf(armSRegister(EEREC_D), armSRegister(EEREC_S));
|
||
return;
|
||
}
|
||
armEmitEeFprNarrow(RWSCRATCH, armDRegister(EEREC_S), a64::x9);
|
||
armAsm->Scvtf(RSSCRATCH, RWSCRATCH);
|
||
armEmitEeFprFromS(armDRegister(EEREC_D), RSSCRATCH, RXSCRATCH);
|
||
}
|
||
|
||
void recCVT_S()
|
||
{
|
||
eeFPURecompileCode(recCVT_S_xmm, Interp::CVT_S,
|
||
XMMINFO_WRITED | XMMINFO_READS);
|
||
}
|
||
|
||
// CVT.W: fd_bits = (int32_t)fpr[fs] truncating toward zero.
|
||
// PS2 clamps overflow to INT32_MAX/MIN — ARM64 Fcvtzs saturates by default,
|
||
// matching interp for the finite-overflow and ±Inf cases. The one divergence
|
||
// is NaN: ARM Fcvtzs yields 0, but the PS2 (interp CVT_W, FPU.cpp) saturates
|
||
// NaN by sign — positive NaN → 0x7fffffff, negative NaN → 0x80000000. Fix up
|
||
// the NaN case only (cold branch over the source-sign select).
|
||
//
|
||
// A relocated slot holds no NaN, so the double tier unscales into the value and
|
||
// converts, and Fcvtzs's own saturation covers it.
|
||
static void recCVT_W_xmm(int info)
|
||
{
|
||
if (CHECK_FPU_FULL)
|
||
{
|
||
armAsm->Fmul(RDSCRATCH, armDRegister(EEREC_S),
|
||
a64::VRegister(NEON_RESERVED_EEFPU_UNSCALE, 64));
|
||
armAsm->Fcvtzs(RWSCRATCH, RDSCRATCH);
|
||
armEmitEeFprWiden(armDRegister(EEREC_D), RWSCRATCH, RXSCRATCH);
|
||
return;
|
||
}
|
||
|
||
const a64::VRegister fs = armSRegister(EEREC_S);
|
||
armAsm->Fcvtzs(RWSCRATCH, fs);
|
||
a64::Label done;
|
||
armAsm->Fcmp(fs, fs); // NaN → unordered (V set)
|
||
armAsm->B(&done, a64::vc); // ordered → keep Fcvtzs result
|
||
armAsm->Fmov(RWARG1, fs); // NaN: broadcast source sign
|
||
armAsm->Asr(RWARG1, RWARG1, 31); // 0 if +, 0xFFFFFFFF if -
|
||
armAsm->Eor(RWSCRATCH, RWARG1, 0x7fffffff); // + → 0x7fffffff, − → 0x80000000
|
||
armAsm->Bind(&done);
|
||
armAsm->Fmov(armSRegister(EEREC_D), RWSCRATCH);
|
||
}
|
||
|
||
void recCVT_W()
|
||
{
|
||
eeFPURecompileCode(recCVT_W_xmm, Interp::CVT_W,
|
||
XMMINFO_WRITED | XMMINFO_READS);
|
||
}
|
||
|
||
#undef _Ft_
|
||
#undef _Fs_
|
||
#undef _Fd_
|
||
|
||
} // namespace COP1
|
||
} // namespace OpcodeImpl
|
||
} // namespace Dynarec
|
||
} // namespace R5900
|