mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
+62
-4
@@ -52,10 +52,10 @@ static void MultiPause()
|
||||
|
||||
static u32 MeasurePauseTime()
|
||||
{
|
||||
// GetCPUTicks may have resolution as low as 1µs
|
||||
// One call to MultiPause could take anywhere from 20ns (fast Haswell) to 400ns (slow Skylake)
|
||||
// We want a measurement of reasonable resolution, but don't want to take too long
|
||||
// So start at a fairly small number and increase it if it's too fast
|
||||
// A tick isn't a fixed time unit (see GetCPUTicks()), so this loop works in raw
|
||||
// ticks and only converts to ns once it has enough. One MultiPause takes 20ns on
|
||||
// a fast Haswell, 400ns on a slow Skylake, 83ns for the eight isb on a Cortex-A78C.
|
||||
// Start small and double the batch until the tick delta clears 100.
|
||||
for (int testcnt = 64; true; testcnt *= 2)
|
||||
{
|
||||
u64 start = GetCPUTicks();
|
||||
@@ -103,6 +103,64 @@ u32 ShortSpin()
|
||||
return time;
|
||||
}
|
||||
|
||||
#if defined(ARCH_ARM64) && !defined(_MSC_VER)
|
||||
// Stop executing until another core stores to `word`, using the local exclusive
|
||||
// monitor as the watchpoint: LDAXR arms it, and the store that clears it raises
|
||||
// the event WFE is waiting on. A store landing between the LDAXR and the WFE
|
||||
// clears the monitor too, so the wake cannot be missed.
|
||||
//
|
||||
// WFE is not a yield or a kernel block: the thread stays runnable, so a
|
||||
// co-resident thread is preempted exactly as it was against the isb spin
|
||||
// (measured: it keeps ~50% of its throughput either way, against 100% when
|
||||
// the waiter blocks in a futex instead).
|
||||
//
|
||||
// The SEVL/WFE pair comes first because the event register is one sticky bit:
|
||||
// anything that cleared the monitor earlier leaves it set, and the WFE below
|
||||
// would then return without parking. SEVL sets it, the first WFE consumes it.
|
||||
//
|
||||
// Nothing clears the monitor on the way out, deliberately. Clearing it is
|
||||
// itself a wake-up event, so a CLREX here would set the event register for the
|
||||
// next iteration and the loop would stop parking altogether — on a Cortex-A78C
|
||||
// waiting on a poster 100µs away, 3.5 wake-ups per wait as written against 6708
|
||||
// with a CLREX added. Linux's arm64 __cmpwait omits it for the same reason.
|
||||
//
|
||||
// A monitor that never fires is a latency cost, not a hang: WFE also wakes on
|
||||
// the periodic event stream, every ~33µs on this host.
|
||||
static void MonitoredWait(const std::atomic<s32>& word, s32 expected)
|
||||
{
|
||||
s32 seen;
|
||||
__asm__ __volatile__(
|
||||
"sevl\n"
|
||||
"wfe\n"
|
||||
"ldaxr %w0, [%1]\n"
|
||||
"cmp %w0, %w2\n"
|
||||
"b.ne 1f\n"
|
||||
"wfe\n"
|
||||
"1:\n"
|
||||
: "=&r"(seen)
|
||||
: "r"(&word), "r"(expected)
|
||||
: "cc", "memory");
|
||||
(void)seen;
|
||||
}
|
||||
#endif
|
||||
|
||||
u32 ShortSpinOn(const std::atomic<s32>& word, s32 expected)
|
||||
{
|
||||
#if defined(ARCH_ARM64) && !defined(_MSC_VER)
|
||||
const u64 start = GetCPUTicks();
|
||||
MonitoredWait(word, expected);
|
||||
// Charge unmeasurably short waits as one tick, not zero: the caller
|
||||
// accumulates this against SPIN_TIME_NS, and a zero would stall that count
|
||||
// forever.
|
||||
const u64 elapsed = std::max<u64>(GetCPUTicks() - start, 1);
|
||||
return static_cast<u32>((elapsed * 1000000000) / GetTickFrequency());
|
||||
#else
|
||||
(void)word;
|
||||
(void)expected;
|
||||
return ShortSpin();
|
||||
#endif
|
||||
}
|
||||
|
||||
static u32 GetSpinTime()
|
||||
{
|
||||
if (char* req = getenv("WAIT_SPIN_MICROSECONDS"))
|
||||
|
||||
@@ -186,6 +186,16 @@ extern u64 GetAvailablePhysicalMemory();
|
||||
/// Spin for a short period of time (call while spinning waiting for a lock)
|
||||
/// Returns the approximate number of ns that passed
|
||||
extern u32 ShortSpin();
|
||||
/// ShortSpin() for a wait whose entire predicate is one atomic word that another
|
||||
/// thread stores to. Where the host can watch an address, this parks the core
|
||||
/// until that store lands; `expected` is the value already seen, and the wait
|
||||
/// ends once `word` no longer holds it. Returns the approximate number of ns
|
||||
/// that passed.
|
||||
///
|
||||
/// May return early for no reason: re-check the predicate in a loop, exactly
|
||||
/// as around ShortSpin(). Splitting the wait across two locations parks on a
|
||||
/// store to neither — `word` must carry it alone.
|
||||
extern u32 ShortSpinOn(const std::atomic<s32>& word, s32 expected);
|
||||
/// Number of ns to spin for before sleeping a thread
|
||||
extern const u32 SPIN_TIME_NS;
|
||||
/// Like C abort() but adds the given message to the crashlog
|
||||
|
||||
@@ -84,7 +84,7 @@ void Threading::WorkSema::WaitForWorkWithSpin()
|
||||
m_sema.Wait();
|
||||
break;
|
||||
}
|
||||
waited += ShortSpin();
|
||||
waited += ShortSpinOn(m_state, value);
|
||||
value = m_state.load(std::memory_order_relaxed);
|
||||
}
|
||||
// Clear back to STATE_RUNNING_0 (but preserve waiting empty flag)
|
||||
@@ -117,7 +117,7 @@ bool Threading::WorkSema::WaitForEmptyWithSpin()
|
||||
return !IsDead(value); // STATE_SLEEPING or STATE_SPINNING, queue is empty!
|
||||
if (waited > SPIN_TIME_NS && m_state.compare_exchange_weak(value, value | STATE_FLAG_WAITING_EMPTY, std::memory_order_acquire))
|
||||
break;
|
||||
waited += ShortSpin();
|
||||
waited += ShortSpinOn(m_state, value);
|
||||
value = m_state.load(std::memory_order_acquire);
|
||||
}
|
||||
pxAssertMsg(!(value & STATE_FLAG_WAITING_EMPTY), "Multiple threads attempted to wait for empty (not currently supported)");
|
||||
@@ -156,7 +156,7 @@ void Threading::UserspaceSemaphore::WaitWithSpin()
|
||||
}
|
||||
if (waited >= SPIN_TIME_NS)
|
||||
break;
|
||||
waited += ShortSpin();
|
||||
waited += ShortSpinOn(m_counter, counter);
|
||||
counter = m_counter.load(std::memory_order_relaxed);
|
||||
}
|
||||
// Spin window expired — block in the kernel (same as plain Wait()).
|
||||
|
||||
+17
-8
@@ -1380,14 +1380,23 @@ target_include_directories(PCSX2_FLAGS INTERFACE
|
||||
set_source_files_properties(PrecompiledHeader.cpp PROPERTIES HEADER_FILE_ONLY TRUE)
|
||||
|
||||
# VUops.cpp (VU interp) and FPU.cpp (EE COP1 interp) must produce bit-exact
|
||||
# results matching PS2 hardware (which has no FMA). The project-wide
|
||||
# -ffp-contract=fast is fine on x86 (no FMA emitted without -mfma) but on aarch64
|
||||
# it lets the compiler contract `acc + fs * ft` to `fmadd` (single-rounded),
|
||||
# breaking bit-exactness vs the recompilers (separate fmul + fadd). In VUops.cpp
|
||||
# this would produce 1-ULP MADDA divergences. FPU.cpp has the identical hazard in
|
||||
# MADDA_S/MSUBA_S (`_FAValf_ += fs*ft` / `-= fs*ft` are single-expression
|
||||
# accumulates that fuse on aarch64 while the EE FPU rec emits two roundings) — a
|
||||
# +1-ULP float drift vs the rec.
|
||||
# results matching PS2 hardware, which does not fuse. The project-wide
|
||||
# -ffp-contract=fast contracts `acc + fs * ft` into a single-rounded fmadd
|
||||
# where the recompilers emit two roundings, so every contraction diverges from
|
||||
# both the JIT and the console. The SCPH-90000 capture that settles which one
|
||||
# the console does, and the count of fused instructions this line removes from
|
||||
# VUops.cpp, are at the head of vu_madd_contract_console_tests.cpp.
|
||||
#
|
||||
# Not an aarch64-only hazard, so this line is not an ARCH_ARM64 candidate:
|
||||
# -march=native supplies FMA on any host that has it, and the default
|
||||
# non-multi-ISA build takes it (cmake/BuildParameters.cmake,
|
||||
# DISABLE_ADVANCE_SIMD=OFF) -- upstream's own amd64 dev build contracts
|
||||
# FPU.cpp's MADDA_S. Only multi-ISA (-msse4.1) and MSVC's x64 default arch
|
||||
# lack the instruction, hence the NOT MSVC guard.
|
||||
#
|
||||
# FPU.cpp has no fusion sites left: its COP1 arithmetic no longer runs through
|
||||
# host floats (eeMulAccumulate and friends). It stays on the list to keep that
|
||||
# true.
|
||||
if(NOT MSVC)
|
||||
set_source_files_properties(VUops.cpp FPU.cpp PROPERTIES COMPILE_OPTIONS "-ffp-contract=off")
|
||||
endif()
|
||||
|
||||
+21
-1
@@ -468,7 +468,27 @@ namespace R5900
|
||||
const int way = addr & 0x1;
|
||||
CacheLine line = cache.lineAt(index, way);
|
||||
|
||||
line.tag.setAddr(cpuRegs.CP0.n.TagLo);
|
||||
// TagLo carries a guest physical page. Our tags do not: they hold the
|
||||
// host pointer the fill translated to (CacheLine::load stores `ppf`),
|
||||
// which is what writeBackIfNeeded dereferences, so copying the guest
|
||||
// word in raw aimed a 64-byte store at an address the guest chose --
|
||||
// setAddr zeroes the top 32 bits, so somewhere below 4 GiB: an
|
||||
// emulator crash normally, or a write into whatever happened to be
|
||||
// mapped there. Translate it the way a fill does instead, through the
|
||||
// KSEG0 alias of the physical page (Memory.cpp maps 0x80000000 onto
|
||||
// physical 0), so the write-back lands at the physical address the
|
||||
// guest named, and take isValidPFN from the same translation so the
|
||||
// two cannot disagree. A tag that does not resolve to plain memory --
|
||||
// an MMIO handler page, or a physical address that does not exist --
|
||||
// is marked unbacked; the line still caches and reports its flags,
|
||||
// and loses its data on eviction (see the comment on CacheTag).
|
||||
const u32 pageTag = cpuRegs.CP0.n.TagLo & ~static_cast<u32>(CacheTag::ALL_BITS);
|
||||
const u32 alias = 0x80000000u | (pageTag & 0x1FFFFFFFu);
|
||||
const VTLBVirtual vmv = vtlbdata.vmap[alias >> VTLB_PAGE_BITS];
|
||||
const bool backed = !vmv.isHandler(alias);
|
||||
|
||||
line.tag.setValidPFN(backed);
|
||||
line.tag.setAddr(backed ? vmv.assumePtr(alias) : static_cast<uptr>(pageTag));
|
||||
line.tag.rawValue &= ~CacheTag::ALL_FLAGS;
|
||||
line.tag.rawValue |= (cpuRegs.CP0.n.TagLo & CacheTag::ALL_FLAGS);
|
||||
|
||||
|
||||
+848
-216
File diff suppressed because it is too large
Load Diff
@@ -290,6 +290,11 @@ struct cpuRegistersPack
|
||||
// use (the JIT cache is too far from the data segment for adrp to reach).
|
||||
// x86 builds carry the bytes and never touch them.
|
||||
alignas(16) EeCop2RecState cop2Rec;
|
||||
|
||||
// {0,1,...,15}. QFSRV's TBL index is this ramp plus a broadcast sa, and
|
||||
// it is here rather than in a literal for the same reason as the block
|
||||
// above: one load against RSTATE instead of a per-site literal.
|
||||
alignas(16) u8 byteRamp[16];
|
||||
};
|
||||
|
||||
alignas(16) extern cpuRegistersPack _cpuRegistersPack;
|
||||
|
||||
+2
-1
@@ -2480,7 +2480,8 @@ void VMManager::Internal::Throttle()
|
||||
return;
|
||||
}
|
||||
|
||||
// Conversion of delta from CPU ticks (microseconds) to milliseconds
|
||||
// Conversion of delta from CPU ticks to milliseconds; a tick's time value is
|
||||
// host-defined, so this has to divide by GetTickFrequency() rather than scale by a constant.
|
||||
const s32 msec = static_cast<s32>((sDeltaTime * -1000) / static_cast<s64>(GetTickFrequency()));
|
||||
|
||||
// If any integer value of milliseconds exists, sleep it off.
|
||||
|
||||
+13
-25
@@ -980,7 +980,9 @@ static __fi void _vuSQRT(VURegs* VU)
|
||||
|
||||
VU->statusflag &= ~0x30;
|
||||
|
||||
if (ft < 0.0)
|
||||
// Sign bit, not `ft < 0.0`: -0 raises I, and so do the denormals vuDouble
|
||||
// has already flushed to it.
|
||||
if (VU->VF[_Ft_].UL[_Ftf_] & 0x80000000)
|
||||
VU->statusflag |= 0x10;
|
||||
VU->q.F = sqrt(fabs(ft));
|
||||
VU->q.F = vuDouble(VU->q.UL);
|
||||
@@ -996,36 +998,22 @@ static __fi void _vuRSQRT(VURegs* VU)
|
||||
|
||||
VU->statusflag &= ~0x30;
|
||||
|
||||
// RSQRT is a square root then a divide, so I comes from the divisor's sign
|
||||
// bit before the zero test and independently of it: -0 raises I for the
|
||||
// root and D below for the division.
|
||||
if (VU->VF[_Ft_].UL[_Ftf_] & 0x80000000)
|
||||
VU->statusflag |= 0x10;
|
||||
|
||||
if (ft == 0.0)
|
||||
{
|
||||
VU->statusflag |= 0x20;
|
||||
// Exclusive, as in DIV: 0/0 is invalid, x/0 is a divide by zero.
|
||||
VU->statusflag |= (fs == 0.0) ? 0x10 : 0x20;
|
||||
|
||||
if (fs != 0)
|
||||
{
|
||||
if ((VU->VF[_Ft_].UL[_Ftf_] & 0x80000000) ^
|
||||
(VU->VF[_Fs_].UL[_Fsf_] & 0x80000000))
|
||||
VU->q.UL = 0xFF7FFFFF;
|
||||
else
|
||||
VU->q.UL = 0x7F7FFFFF;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((VU->VF[_Ft_].UL[_Ftf_] & 0x80000000) ^
|
||||
(VU->VF[_Fs_].UL[_Fsf_] & 0x80000000))
|
||||
VU->q.UL = 0x80000000;
|
||||
else
|
||||
VU->q.UL = 0;
|
||||
|
||||
VU->statusflag |= 0x10;
|
||||
}
|
||||
// Sign of the dividend alone -- the divisor is a root, never negative.
|
||||
VU->q.UL = (VU->VF[_Fs_].UL[_Fsf_] & 0x80000000) | 0x7F7FFFFF;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ft < 0.0)
|
||||
{
|
||||
VU->statusflag |= 0x10;
|
||||
}
|
||||
|
||||
temp = sqrt(fabs(ft));
|
||||
VU->q.F = fs / temp;
|
||||
VU->q.F = vuDouble(VU->q.UL);
|
||||
|
||||
+28
-60
@@ -2524,25 +2524,20 @@ void recCOP2_VSQRT()
|
||||
|
||||
const int ftf = _Ftf_cop2;
|
||||
|
||||
// Clear D/I flags
|
||||
// Clear D/I, then take I from the sign bit: a compare against zero misses
|
||||
// -0 and reads an unordered result as negative.
|
||||
armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.statusflag));
|
||||
armAsm->Mov(RWARG1, 0x30); armAsm->Bic(RWSCRATCH, RWSCRATCH, RWARG1); // clear D/I bits
|
||||
armAsm->Ldr(a64::w1, armVU0Mem(&VU0.VF[_Ft_cop2].UL[ftf]));
|
||||
a64::Label ftPositive;
|
||||
armAsm->Tbz(a64::w1, 31, &ftPositive);
|
||||
armAsm->Orr(RWSCRATCH, RWSCRATCH, 0x10);
|
||||
armAsm->Bind(&ftPositive);
|
||||
armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.statusflag));
|
||||
|
||||
// Load ft scalar
|
||||
armAsm->Ldr(RSSCRATCH, armVU0Mem(&VU0.VF[_Ft_cop2].UL[ftf]));
|
||||
|
||||
// If ft < 0, set invalid flag (D flag = 0x10)
|
||||
a64::Label notNeg;
|
||||
armAsm->Fcmp(RSSCRATCH, 0.0);
|
||||
armAsm->B(a64::ge, ¬Neg);
|
||||
{
|
||||
armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.statusflag));
|
||||
armAsm->Orr(RWSCRATCH, RWSCRATCH, 0x10);
|
||||
armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.statusflag));
|
||||
}
|
||||
armAsm->Bind(¬Neg);
|
||||
|
||||
// Q = sqrt(|ft|)
|
||||
armAsm->Fabs(RSSCRATCH, RSSCRATCH);
|
||||
armAsm->Fsqrt(RSSCRATCH, RSSCRATCH);
|
||||
@@ -2564,9 +2559,15 @@ void recCOP2_VRSQRT()
|
||||
const int fsf = _Fsf_cop2;
|
||||
const int ftf = _Ftf_cop2;
|
||||
|
||||
// Clear D/I flags
|
||||
// Clear D/I, then take I from the divisor's sign bit, before the zero test
|
||||
// below and independently of it. See _vuRSQRT.
|
||||
armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.statusflag));
|
||||
armAsm->Mov(RWARG1, 0x30); armAsm->Bic(RWSCRATCH, RWSCRATCH, RWARG1); // clear D/I bits
|
||||
armAsm->Ldr(a64::w1, armVU0Mem(&VU0.VF[_Ft_cop2].UL[ftf]));
|
||||
a64::Label ftPositive;
|
||||
armAsm->Tbz(a64::w1, 31, &ftPositive);
|
||||
armAsm->Orr(RWSCRATCH, RWSCRATCH, 0x10);
|
||||
armAsm->Bind(&ftPositive);
|
||||
armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.statusflag));
|
||||
|
||||
// Load ft scalar
|
||||
@@ -2580,61 +2581,28 @@ void recCOP2_VRSQRT()
|
||||
armAsm->Fcmp(RSSCRATCH2, 0.0);
|
||||
armAsm->B(a64::ne, &ftNonZero);
|
||||
|
||||
// ft == 0: set div-by-zero flag (0x20), Q based on signs
|
||||
// ft == 0: 0/0 is invalid, x/0 is a divide by zero, exclusively. Q
|
||||
// saturates either way, signed by the dividend -- no xor, unlike VDIV.
|
||||
{
|
||||
armAsm->Ldr(a64::w1, armVU0Mem(&VU0.VF[_Fs_cop2].UL[fsf]));
|
||||
armAsm->And(a64::w2, a64::w1, 0x80000000);
|
||||
armAsm->Mov(a64::w3, 0x7F7FFFFF);
|
||||
armAsm->Orr(a64::w2, a64::w2, a64::w3);
|
||||
|
||||
armAsm->Fcmp(RSSCRATCH, 0.0);
|
||||
armAsm->Mov(a64::w1, 0x10);
|
||||
armAsm->Mov(a64::w3, 0x20);
|
||||
armAsm->Csel(a64::w1, a64::w1, a64::w3, a64::eq);
|
||||
armAsm->Orr(RWSCRATCH, RWSCRATCH, a64::w1);
|
||||
|
||||
// fs == 0: set invalid flag too (0x10), Q = ±0
|
||||
a64::Label fsNonZero;
|
||||
armAsm->B(a64::ne, &fsNonZero);
|
||||
{
|
||||
// D/I flags: 0x30 (both invalid and div-by-zero)
|
||||
armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.statusflag));
|
||||
armAsm->Orr(RWSCRATCH, RWSCRATCH, 0x30);
|
||||
armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.statusflag));
|
||||
|
||||
// Q = sign(fs) XOR sign(ft) ? -0 : +0
|
||||
armAsm->Ldr(a64::w1, armVU0Mem(&VU0.VF[_Fs_cop2].UL[fsf]));
|
||||
armAsm->Ldr(a64::w2, armVU0Mem(&VU0.VF[_Ft_cop2].UL[ftf]));
|
||||
armAsm->Eor(a64::w1, a64::w1, a64::w2);
|
||||
armAsm->And(RWSCRATCH, a64::w1, 0x80000000); // just sign bit, or 0
|
||||
armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.q));
|
||||
armAsm->B(&done);
|
||||
}
|
||||
|
||||
// fs != 0: Q = ±FLT_MAX
|
||||
armAsm->Bind(&fsNonZero);
|
||||
{
|
||||
armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.statusflag));
|
||||
armAsm->Orr(RWSCRATCH, RWSCRATCH, 0x20);
|
||||
armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.statusflag));
|
||||
|
||||
armAsm->Ldr(a64::w1, armVU0Mem(&VU0.VF[_Fs_cop2].UL[fsf]));
|
||||
armAsm->Ldr(a64::w2, armVU0Mem(&VU0.VF[_Ft_cop2].UL[ftf]));
|
||||
armAsm->Eor(a64::w1, a64::w1, a64::w2);
|
||||
armAsm->Mov(a64::w2, 0x7F7FFFFF);
|
||||
armAsm->Mov(a64::w3, 0xFF7FFFFF);
|
||||
armAsm->Tst(a64::w1, 0x80000000);
|
||||
armAsm->Csel(RWSCRATCH, a64::w3, a64::w2, a64::ne);
|
||||
armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.q));
|
||||
armAsm->B(&done);
|
||||
}
|
||||
armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.statusflag));
|
||||
armAsm->Str(a64::w2, armVU0Mem(&VU0.q));
|
||||
}
|
||||
armAsm->B(&done);
|
||||
|
||||
// ft != 0: normal path
|
||||
armAsm->Bind(&ftNonZero);
|
||||
{
|
||||
// If ft < 0, set invalid flag
|
||||
a64::Label notNeg;
|
||||
armAsm->Fcmp(RSSCRATCH2, 0.0);
|
||||
armAsm->B(a64::ge, ¬Neg);
|
||||
{
|
||||
armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.statusflag));
|
||||
armAsm->Orr(RWSCRATCH, RWSCRATCH, 0x10);
|
||||
armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.statusflag));
|
||||
}
|
||||
armAsm->Bind(¬Neg);
|
||||
|
||||
// Q = fs / sqrt(|ft|)
|
||||
armAsm->Fabs(RSSCRATCH2, RSSCRATCH2);
|
||||
armAsm->Fsqrt(RSSCRATCH2, RSSCRATCH2);
|
||||
|
||||
+69
-28
@@ -38,9 +38,9 @@ namespace Interp = R5900::Interpreter::OpcodeImpl::COP1;
|
||||
//------------------------------------------------------------------
|
||||
// FCR31 block residency (GE-12)
|
||||
//------------------------------------------------------------------
|
||||
// The leak/4248 guest-class-0xc design: the C.cond/BC1/CFC1/CTC1/DIV/SQRT
|
||||
// family accesses fprc[31] through the GPR allocator (ARM64TYPE_FPRC — the
|
||||
// load/writeback plumbing existed in iCore but had no allocation site).
|
||||
// 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
|
||||
@@ -1027,13 +1027,10 @@ static void recSQRT_S_xmm(int info)
|
||||
|
||||
// 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, even though they flush to −0 and produce +0.
|
||||
// This used to carry an extra `exp != 0` gate, which cost exactly those two
|
||||
// operand classes their flag; x86's recSQRT_S_xmm (iFPU.cpp, MOVMSKPS & 1)
|
||||
// and the FULL-mode DOUBLE path (iFPUd-arm64.cpp) have always tested the
|
||||
// sign alone. Scored against a first-party capture over the sign × exponent
|
||||
// matrix — see EeRecFpu.SqrtSInvalidFlagFollowsTheSignBitAlone.
|
||||
// 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
|
||||
@@ -1150,21 +1147,52 @@ static void recRSQRT_S_xmm(int info)
|
||||
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));
|
||||
|
||||
// Clear I|D (sticky SI|SD are left intact).
|
||||
armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||||
armAsm->Bic(RWSCRATCH, RWSCRATCH, FPUflagI | FPUflagD);
|
||||
armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||||
a64::Label notZero, xOverZero, flagsDone, ftPositive, end;
|
||||
|
||||
a64::Label notZero, doDiv, 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);
|
||||
|
||||
// Zero divisor: set D|SD; result = sign(FS) | 0x7f7fffff.
|
||||
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
|
||||
@@ -1177,30 +1205,43 @@ static void recRSQRT_S_xmm(int info)
|
||||
// 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.
|
||||
armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||||
armAsm->Orr(RWSCRATCH, RWSCRATCH, FPUflagD | FPUflagSD);
|
||||
armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||||
armAsm->Fmov(RWARG2, armSRegister(dreg)); // raw Fs bits, saved before any write
|
||||
armAsm->And(RWARG2, RWARG2, 0x80000000);
|
||||
armAsm->Orr(RWARG2, RWARG2, 0x7f7fffff);
|
||||
armAsm->Fmov(armSRegister(EEREC_D), RWARG2);
|
||||
armAsm->B(&end);
|
||||
|
||||
armAsm->Bind(¬Zero);
|
||||
// Negative divisor (exp nonzero, sign set): set I|SI. sqrt still takes |Ft|.
|
||||
armAsm->Tbz(RWARG1, 31, &doDiv);
|
||||
armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||||
armAsm->Orr(RWSCRATCH, RWSCRATCH, FPUflagI | FPUflagSI);
|
||||
armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
|
||||
|
||||
armAsm->Bind(&doDiv);
|
||||
armAsm->Fabs(armSRegister(treg), armSRegister(treg)); // |Ft| (no-op if positive)
|
||||
if (CHECK_FPU_EXTRA_OVERFLOW)
|
||||
{
|
||||
fpuClampCompareOperand(armSRegister(dreg));
|
||||
fpuClampCompareOperand(armSRegister(treg));
|
||||
}
|
||||
armAsm->Fsqrt(armSRegister(treg), 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));
|
||||
|
||||
|
||||
@@ -150,7 +150,8 @@ static void ToPS2FPU_Full(int idx, bool flags, int /*absidx*/, bool acc, bool ad
|
||||
//
|
||||
// `hi`, not `hs`: kEeFpuMax itself is representable and belongs to the
|
||||
// halving arm, which handles it exactly (halved it is +FLT_MAX, and
|
||||
// 0x7f7fffff + 0x00800000 == 0x7fffffff).
|
||||
// 0x7f7fffff + 0x00800000 == 0x7fffffff). This is the same bound the
|
||||
// interpreter's eeRoundToSingle saturates at (FPU.cpp).
|
||||
armAsm->Mov(RXARG2, UINT64_C(0x47FFFFFFE0000000)); // (2 - 2^-23) * 2^128
|
||||
armAsm->Cmp(RXARG1, RXARG2);
|
||||
armAsm->B(&toOverflow, a64::hi);
|
||||
@@ -496,8 +497,10 @@ static void recFPUOp(int info, int eeRecDst, int op /*0=add,1=sub*/, bool acc)
|
||||
// mul.s(1.0, x) is one ULP low for 8257536 of the 2^23 significands while
|
||||
// mul.s(x, 1.0) is exact for all of them.
|
||||
//
|
||||
// The interpreter models the same law (FPU.cpp eeMulRound / eeMulOneUlpLow /
|
||||
// eeMulDefectiveFt); this is its mode-3 codegen. FpuMulHack is a one-point
|
||||
// The interpreter models a superset (FPU.cpp eeMulRound / eeMulOneUlpLow /
|
||||
// eeMulArray): it reconstructs the array's truncated low half, so it also
|
||||
// catches the rows where the tail is non-zero but smaller than the borrow.
|
||||
// This is the mode-3 codegen for the zero-tail law. FpuMulHack is a one-point
|
||||
// sample of the same rule and this subsumes it, including the asymmetry -- it
|
||||
// is not folded in here because iFPUd never had it.
|
||||
//
|
||||
@@ -564,7 +567,7 @@ static void recFPUOp(int info, int eeRecDst, int op /*0=add,1=sub*/, bool acc)
|
||||
// one-directional (it can only miss a deficit, never invent one) and rare in
|
||||
// general operand space, per the count above. The term needs a bitfield extract
|
||||
// NEON has no equivalent for, so it has to go through GPRs and come back --
|
||||
// sketched at ten instructions against this predicate's three.
|
||||
// sketched at ten instructions against this predicate's one.
|
||||
// The resulting interpreter divergence is pinned by
|
||||
// EeRecFpuFull.MulDefectDropsTheBoundaryTermTheInterpreterModels.
|
||||
//
|
||||
|
||||
+43
-55
@@ -1306,10 +1306,26 @@ void recPMSUBH()
|
||||
// QFSRV: Rd = {Rs, Rt} >> (sa * 8), truncated to 128 bits.
|
||||
// cpuRegs.sa is in bytes (0-15). Concatenate Rt (low) and Rs (high)
|
||||
// into a 256-bit value, shift right by sa bytes, take lower 128 bits.
|
||||
// Implementation: store {Rt, Rs} to adjacent memory, unaligned load at offset sa.
|
||||
// Matches x86 approach using tempqw buffer.
|
||||
alignas(16) static u8 s_qfsrvTemp[32];
|
||||
|
||||
//
|
||||
// TBL over a two-register table is exactly this operation: the table is 32
|
||||
// bytes with Rt at 0-15 and Rs at 16-31, and byte i of the result is
|
||||
// table[sa + i]. The index vector is a {0..15} ramp plus a broadcast sa, so
|
||||
// the largest index a legal sa can produce is 15 + 15 = 30 and the table
|
||||
// always covers it.
|
||||
//
|
||||
// SSE has no cross-register variable byte shift, so the x86 emitter spills
|
||||
// both operands to a static buffer and reloads 128 bits at offset sa. This
|
||||
// did the same until the table form replaced it. The reload was the
|
||||
// expensive part: a 16-byte load is forwarded from the two stores only when
|
||||
// it is 8-byte aligned, so fourteen of the sixteen sa values drained the
|
||||
// store buffer instead.
|
||||
//
|
||||
// The other consequence is on the And below. An out-of-range index cannot
|
||||
// name a host address here — TBL answers zero for one — so that mask now
|
||||
// keeps the guest semantics (sa is a byte count mod 16) rather than standing
|
||||
// between a guest-written sa and a host out-of-bounds read. The
|
||||
// adjacent-source path above still indexes host memory and still needs it for
|
||||
// the original reason.
|
||||
void recQFSRV()
|
||||
{
|
||||
if (!_Rd_) return;
|
||||
@@ -1318,62 +1334,34 @@ void recQFSRV()
|
||||
mmiFlushReg(_Rt_);
|
||||
mmiInvalidateDest(_Rd_);
|
||||
|
||||
// Adjacent-source fast path: when Rs == Rt+1 the 256-bit
|
||||
// {Rt:Rs} window already exists contiguously in the GPR array
|
||||
// (GPR.r[Rt] immediately precedes GPR.r[Rt+1]==GPR.r[Rs], 32 bytes).
|
||||
// Read the unaligned 128 bits directly at &GPR.r[Rt] + sa and skip the two
|
||||
// temp stores. sa is 0..15 so the load stays within the two registers' 32
|
||||
// bytes. Gate on Rt != 0 to avoid depending on GPR.r[0] holding zero in
|
||||
// memory (the slow path Movi's it).
|
||||
if (_Rt_ != 0 && _Rs_ == _Rt_ + 1)
|
||||
{
|
||||
// The flushes above do NOT make this window memory-coherent: mmiFlushReg
|
||||
// is _deleteEEreg, which reconciles const-prop and the scalar/NEON slots
|
||||
// and never touches the pins. Under lazy-dirty the pin is authoritative
|
||||
// for UD[0] and armStoreEERegPtrRaw elides the canonical store, so a
|
||||
// pinned source's lower half in memory is routinely stale here. Unlike
|
||||
// every other raw quad-load site we cannot merge after the load — the
|
||||
// read straddles two guest registers — so flush the two pins the window
|
||||
// actually covers. It covers exactly r[Rt] and r[Rt+1]: sa <= 15 over
|
||||
// their 32 bytes. Four adjacent pairs are both-pinned — ($at,$v0)
|
||||
// ($v0,$v1) ($v1,$a0) ($a0,$a1) — and eight more have one pinned
|
||||
// operand, which is the register range a funnel-shift memcpy uses. (SM-010)
|
||||
armFlushEEGPRPin(_Rt_);
|
||||
armFlushEEGPRPin(_Rs_);
|
||||
// Adjacent sources (Rs == Rt+1) are contiguous in the GPR array, so a
|
||||
// 16-byte read at &GPR.r[Rt] + sa funnels them without a TBL. That shortcut
|
||||
// was removed: it built a host address out of sa, and a pinned source —
|
||||
// twelve of the adjacent pairs have one — had to be flushed into the window
|
||||
// first, an 8-byte store under a 16-byte load that drains the store buffer
|
||||
// at 13.1 cycles instead of forwarding. Unpinned it ran 2.38 against this
|
||||
// sequence's 2.13 (A78C) and 2.34 against 2.00 (X1C).
|
||||
|
||||
armLoadEERegPtr(RWSCRATCH, &cpuRegs.sa);
|
||||
// Clamp sa to 0..15 before indexing host memory. MTSA masks at the
|
||||
// write, cpuRegs.sa can't hold >= 16 and this is belt-and-braces.
|
||||
// An unmasked sa would walk this 128-bit load out of the two
|
||||
// registers' 32 bytes, a guest-controlled host OOB read. (AX-03)
|
||||
armAsm->And(RWSCRATCH, RWSCRATCH, 0xf);
|
||||
armMoveAddressToReg(RSCRATCHADDR, &cpuRegs.GPR.r[_Rt_]);
|
||||
armAsm->Add(RSCRATCHADDR, RSCRATCHADDR, RXSCRATCH);
|
||||
armAsm->Ldr(RQSCRATCH, a64::MemOperand(RSCRATCHADDR));
|
||||
armStoreEEGPRQuad(RQSCRATCH, _Rd_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store Rt at temp[0:15], Rs at temp[16:31]
|
||||
mmiLoadReg(RQSCRATCH, _Rt_);
|
||||
armMoveAddressToReg(RSCRATCHADDR, &s_qfsrvTemp[0]);
|
||||
armAsm->Str(RQSCRATCH, a64::MemOperand(RSCRATCHADDR));
|
||||
|
||||
mmiLoadReg(RQSCRATCH, _Rs_);
|
||||
armMoveAddressToReg(RSCRATCHADDR, &s_qfsrvTemp[16]);
|
||||
armAsm->Str(RQSCRATCH, a64::MemOperand(RSCRATCHADDR));
|
||||
|
||||
// Load sa (byte offset), clamped to 0..15 — see the fast path above
|
||||
// index = sa + {0..15}, built in RQSCRATCH3 before the operands land so
|
||||
// the broadcast can borrow RQSCRATCH.
|
||||
armAsm->Ldr(RQSCRATCH3, armCpuRegMem(&_cpuRegistersPack.byteRamp));
|
||||
armLoadEERegPtr(RWSCRATCH, &cpuRegs.sa);
|
||||
// SA is 4 bits and MTSA/MTSAB/MTSAH all mask at the write, so this is
|
||||
// belt-and-braces. A TBL index past the 32-byte table answers zero, so no
|
||||
// host address depends on it.
|
||||
armAsm->And(RWSCRATCH, RWSCRATCH, 0xf);
|
||||
armAsm->Dup(RQSCRATCH.V16B(), RWSCRATCH);
|
||||
armAsm->Add(RQSCRATCH3.V16B(), RQSCRATCH3.V16B(), RQSCRATCH.V16B());
|
||||
|
||||
// Unaligned 128-bit load from temp + sa
|
||||
armMoveAddressToReg(RSCRATCHADDR, &s_qfsrvTemp[0]);
|
||||
armAsm->Add(RSCRATCHADDR, RSCRATCHADDR, RXSCRATCH); // addr = temp + sa
|
||||
armAsm->Ldr(RQSCRATCH, a64::MemOperand(RSCRATCHADDR));
|
||||
// The table has to be a consecutive pair, which q30/q31 are. Emitted
|
||||
// directly rather than through armEmitVTBL, whose assert rejects
|
||||
// RQSCRATCH/RQSCRATCH2 as sources: it reserves them for the copy it makes
|
||||
// when the pair is not consecutive, the one case this cannot hit.
|
||||
mmiLoadReg(RQSCRATCH, _Rt_); // table bytes 0..15
|
||||
mmiLoadReg(RQSCRATCH2, _Rs_); // table bytes 16..31
|
||||
armAsm->Tbl(RQSCRATCH3.V16B(), RQSCRATCH.V16B(), RQSCRATCH2.V16B(), RQSCRATCH3.V16B());
|
||||
|
||||
// Store result to Rd
|
||||
armStoreEEGPRQuad(RQSCRATCH, _Rd_);
|
||||
armStoreEEGPRQuad(RQSCRATCH3, _Rd_);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -1106,18 +1106,43 @@ static void emitCycleUpdateAndEventCheck()
|
||||
armEmitCondBranch(a64::ge, DispatcherEvent);
|
||||
}
|
||||
|
||||
void SetBranchReg(EEBranchRegMode mode, u32 call_return_pc)
|
||||
void SetBranchReg(EEBranchRegMode mode, u32 call_return_pc, int wbreg)
|
||||
{
|
||||
const Cop2VfCacheScope vfCacheScope; // fork tail: preserve compile-time cache state
|
||||
g_branch = 1;
|
||||
|
||||
// Where the target is. recJR/recJALR park it in an ARM64TYPE_PCWRITEBACK
|
||||
// slot, whose allocator home is cpuRegs.pcWriteback — so a delay slot that
|
||||
// took the register back has already spilled the value there, and one that
|
||||
// left it alone means the register is still the newest copy. Dropping
|
||||
// MODE_WRITE tells the flush below to release the slot without emitting
|
||||
// that spill.
|
||||
const bool parked = wbreg >= 0 && arm64gprs[wbreg].inuse &&
|
||||
arm64gprs[wbreg].type == ARM64TYPE_PCWRITEBACK;
|
||||
if (parked)
|
||||
arm64gprs[wbreg].mode &= ~MODE_WRITE;
|
||||
|
||||
// Flush all GPR/NEON/constant allocations FIRST, while host registers
|
||||
// still hold correct guest values. iFlushCall writes back delay slot
|
||||
// results (like addiu sp) before the branch target is loaded into w0.
|
||||
iFlushCall(FLUSH_EVERYTHING);
|
||||
|
||||
// Now load branch target from pcWriteback (saved by recJR/recJALR)
|
||||
armLoadEERegPtr(a64::w0, &cpuRegs.pcWriteback);
|
||||
// The tail reads the target wherever it already is — the parked register or
|
||||
// w0 — rather than funnelling it through w0 first. Nothing below writes an
|
||||
// allocatable register before it's read: the ring and event-check code use
|
||||
// x8/x9/x10/x17, all carved out of the allocator pool.
|
||||
if (parked)
|
||||
{
|
||||
// iFlushCall allocates no registers, so the released slot still holds
|
||||
// the target.
|
||||
pxAssert(!arm64gprs[wbreg].inuse);
|
||||
}
|
||||
else
|
||||
{
|
||||
armLoadEERegPtr(a64::w0, &cpuRegs.pcWriteback);
|
||||
}
|
||||
const a64::Register target_w = parked ? armWRegister(wbreg) : a64::w0;
|
||||
const a64::Register target_x = parked ? armXRegister(wbreg) : a64::x0;
|
||||
|
||||
// GoemonTlbHack: recJR/recJALR store the raw virtual register target; the
|
||||
// JIT dispatches in physical space, so translate it before use. Mirrors
|
||||
@@ -1133,18 +1158,21 @@ void SetBranchReg(EEBranchRegMode mode, u32 call_return_pc)
|
||||
{
|
||||
pxAssert(mode == EEBranchRegMode::Jump);
|
||||
armFlushEEClobberedPins(); // lazy-dirty seam: pairs with the reload below
|
||||
armAsm->Mov(a64::w0, target_w); // RWARG1: the virtual target in, the paddr out
|
||||
armEmitCall((void*)vtlb_V2P);
|
||||
// vtlb_V2P writes no guest GPRs but clobbers the caller-saved pins;
|
||||
// the DispatcherReg jump below can cache-hit straight into a block.
|
||||
armReloadEEClobberedPins();
|
||||
}
|
||||
const a64::Register& branch_w = EmuConfig.Gamefixes.GoemonTlbHack ? a64::w0 : target_w;
|
||||
const a64::Register& branch_x = EmuConfig.Gamefixes.GoemonTlbHack ? a64::x0 : target_x;
|
||||
|
||||
// Store to cpuRegs.pc
|
||||
armAsm->Str(a64::w0, armCpuRegMem(&cpuRegs.pc));
|
||||
armAsm->Str(branch_w, armCpuRegMem(&cpuRegs.pc));
|
||||
|
||||
// Alignment check
|
||||
a64::Label unaligned;
|
||||
armAsm->Tst(a64::w0, 3);
|
||||
armAsm->Tst(branch_w, 3);
|
||||
armAsm->B(&unaligned, a64::ne);
|
||||
|
||||
if (mode == EEBranchRegMode::Return)
|
||||
@@ -1170,7 +1198,7 @@ void SetBranchReg(EEBranchRegMode mode, u32 call_return_pc)
|
||||
// and desync every subsequent return (the FEX detail our first
|
||||
// sketch got wrong).
|
||||
a64::Label miss;
|
||||
armAsm->Cmp(RXSCRATCH, a64::x0);
|
||||
armAsm->Cmp(RXSCRATCH, branch_x);
|
||||
armAsm->B(&miss, a64::ne);
|
||||
armAsm->Ret(RSCRATCHADDR);
|
||||
|
||||
@@ -3001,6 +3029,11 @@ static void recResetRaw()
|
||||
// ([RSTATE, #imm]) — (re)write them before any block compiles.
|
||||
cop2RecWritePackConstants();
|
||||
|
||||
// recQFSRV reads this ramp as its TBL index base ([RSTATE, #imm]); rewrite
|
||||
// it here for the same reason as cop2RecWritePackConstants above.
|
||||
for (u8 i = 0; i < 16; i++)
|
||||
_cpuRegistersPack.byteRamp[i] = i;
|
||||
|
||||
// Full reset regenerates every block and dispatcher, so nothing can
|
||||
// reference old pool content — drop it. Required since FX-03a: the
|
||||
// manual-check snapshot blobs are not dedup'd, so without this the pool
|
||||
|
||||
@@ -329,19 +329,6 @@ static __fi void armFlushEEGPRPins()
|
||||
armAsm->Str(pin.host, armCpuRegMem(&cpuRegs.GPR.r[pin.gpr].UD[0]));
|
||||
}
|
||||
|
||||
// Flush ONE pin mirror back to canonical memory. Not a dirty-subset heuristic
|
||||
// (see the note above) — this is for the emitter that reads a STATICALLY KNOWN
|
||||
// guest-GPR memory window raw, where neither of the two normal coherence tools
|
||||
// applies: pin substitution (armLoadEERegPtr) can't serve an unaligned read,
|
||||
// and the post-load lane merge (armMergeEEResidentIntoQuad) can't fix a quad
|
||||
// that straddles two guest registers. The caller must name every register its
|
||||
// window covers. Emits nothing when gpr is not pinned.
|
||||
static __fi void armFlushEEGPRPin(int gpr)
|
||||
{
|
||||
if (const vixl::aarch64::Register* pin = armEEPinForGPR(gpr))
|
||||
armAsm->Str(*pin, armCpuRegMem(&cpuRegs.GPR.r[gpr].UD[0]));
|
||||
}
|
||||
|
||||
// Flush only the CALLER-saved pins. Required before any C call that is
|
||||
// followed by armReloadEEClobberedPins: the reload reads canonical memory,
|
||||
// which under lazy-dirty is stale until flushed — the pair would otherwise
|
||||
@@ -685,7 +672,8 @@ enum class EEBranchRegMode
|
||||
Return,
|
||||
Call,
|
||||
};
|
||||
void SetBranchReg(EEBranchRegMode mode = EEBranchRegMode::Jump, u32 call_return_pc = 0);
|
||||
// wbreg is the register recJR/recJALR captured the target in, or -1 without one.
|
||||
void SetBranchReg(EEBranchRegMode mode = EEBranchRegMode::Jump, u32 call_return_pc = 0, int wbreg = -1);
|
||||
void SetBranchImm(u32 imm);
|
||||
// recJAL tail: SetBranchImm plus a call-ret frame push and a BL-form link
|
||||
// to the callee (falls back to SetBranchImm for WaitLoop-FF-shaped blocks).
|
||||
|
||||
@@ -68,23 +68,28 @@ void recJR()
|
||||
{
|
||||
const u32 rs = _Rs_;
|
||||
|
||||
// Save jump target to memory BEFORE delay slot, so it can't be lost
|
||||
// if the delay slot evicts registers. A pinned/allocator-resident rs is
|
||||
// stored directly (WS-C5) — _deleteEEreg(rs, 1) flushed first, so the
|
||||
// pin is coherent (post-flush contract of _eeGetGPRSourceReg).
|
||||
_deleteEEreg(rs, 1); // flush rs to memory
|
||||
armStoreEERegPtr(_eeGetGPRSourceReg(RWSCRATCH, rs), &cpuRegs.pcWriteback);
|
||||
// A delay slot that neither writes rs nor needs the branch's own ordering
|
||||
// is emitted ahead of the jump instead, which takes it out of the window
|
||||
// the capture below has to survive.
|
||||
const bool swapped = !EmuConfig.Gamefixes.GoemonTlbHack && TrySwapDelaySlot(rs, 0, 0, true);
|
||||
|
||||
recompileNextInstruction(true, false);
|
||||
// Capture the jump target before the delay slot, which MIPS lets write rs,
|
||||
// into an ARM64TYPE_PCWRITEBACK slot -- see SetBranchReg for how it's read
|
||||
// back.
|
||||
const int wbreg = _allocArm64GPR(ARM64TYPE_PCWRITEBACK, 0, MODE_WRITE);
|
||||
_eeMoveGPRtoR(armWRegister(wbreg), rs);
|
||||
|
||||
if (!swapped)
|
||||
recompileNextInstruction(true, false);
|
||||
|
||||
// JR $ra is the ABI return idiom — pop the call-ret ring and RET so the
|
||||
// hardware RAS (pushed by the paired call-site BL) predicts the target.
|
||||
// Emit-gated off under GoemonTlbHack: SetBranchReg compares V2P-translated
|
||||
// targets there, which can never match the virtual frame RAs.
|
||||
if (rs == 31 && !EmuConfig.Gamefixes.GoemonTlbHack)
|
||||
SetBranchReg(EEBranchRegMode::Return);
|
||||
SetBranchReg(EEBranchRegMode::Return, 0, wbreg);
|
||||
else
|
||||
SetBranchReg();
|
||||
SetBranchReg(EEBranchRegMode::Jump, 0, wbreg);
|
||||
}
|
||||
|
||||
//// JALR — jump to rs, link in rd
|
||||
@@ -92,14 +97,21 @@ void recJALR()
|
||||
{
|
||||
const u32 rs = _Rs_;
|
||||
const u32 rd = _Rd_;
|
||||
const u32 newpc = pc + 4;
|
||||
const u32 newpc = pc + 4; // captured before a swap can advance pc past the slot
|
||||
|
||||
// Save jump target to memory BEFORE delay slot.
|
||||
// Must read rs before writing rd in case rd == rs — the Str below
|
||||
// captures the target into pcWriteback before the rd write can refresh
|
||||
// a shared pin. (WS-C5; post-flush pin coherence via _deleteEEreg.)
|
||||
_deleteEEreg(rs, 1); // flush rs to memory
|
||||
armStoreEERegPtr(_eeGetGPRSourceReg(RWSCRATCH, rs), &cpuRegs.pcWriteback);
|
||||
// See recJR. rd joins rs in the safety check because the link is written
|
||||
// before the delay slot runs, so a slot that reads or writes rd cannot be
|
||||
// hoisted past that write. The rd == rs term is x86's, kept for parity: it
|
||||
// is what stops x86's swapped arm from reading rs after the link write
|
||||
// (iR5900Jump.cpp:174).
|
||||
const bool swapped = !EmuConfig.Gamefixes.GoemonTlbHack && rd != rs &&
|
||||
TrySwapDelaySlot(rs, 0, rd, true);
|
||||
|
||||
// Capture the jump target before the delay slot; see recJR. Ordered ahead
|
||||
// of the rd write below because rd may be rs, and the capture has to see
|
||||
// the pre-link value.
|
||||
const int wbreg = _allocArm64GPR(ARM64TYPE_PCWRITEBACK, 0, MODE_WRITE);
|
||||
_eeMoveGPRtoR(armWRegister(wbreg), rs);
|
||||
|
||||
// Write link address to rd
|
||||
if (rd)
|
||||
@@ -118,16 +130,17 @@ void recJALR()
|
||||
}
|
||||
}
|
||||
|
||||
recompileNextInstruction(true, false);
|
||||
if (!swapped)
|
||||
recompileNextInstruction(true, false);
|
||||
|
||||
// JALR linking into $ra is the ABI indirect-call idiom — push a call-ret
|
||||
// frame and transfer via BL so the callee's return RETs to our landing.
|
||||
// Other link registers don't pair with the JR-$ra pop, so they take the
|
||||
// plain jump (their returns just compare-miss if the callee uses jr $ra).
|
||||
if (rd == 31 && !EmuConfig.Gamefixes.GoemonTlbHack)
|
||||
SetBranchReg(EEBranchRegMode::Call, newpc);
|
||||
SetBranchReg(EEBranchRegMode::Call, newpc, wbreg);
|
||||
else
|
||||
SetBranchReg();
|
||||
SetBranchReg(EEBranchRegMode::Jump, 0, wbreg);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -96,7 +96,11 @@
|
||||
// chains, and folds AND_XYZW/SHIFT_XYZW into that vector. Every
|
||||
// flag-writing FMAC changes shape, and the emitted [x25, #imm] weight
|
||||
// offsets only exist in mVUglob layouts carrying macWeights.
|
||||
static constexpr u32 kMvuCompilerAbiVersion = 16;
|
||||
// 17 — MADD/MSUB and the A-forms drop the mVUclamp3 on the product ahead
|
||||
// of the accumulate; only shapes compiled under vuClampMode:2 change.
|
||||
// 18 — RSQRT's zero path ORs its D/I into divFlag instead of assigning over
|
||||
// it, so the sign test's I survives. Only RSQRT changes shape.
|
||||
static constexpr u32 kMvuCompilerAbiVersion = 18;
|
||||
|
||||
// Hash/equality functors for XXH128_hash_t — let std::unordered_map<XXH128_hash_t, …>
|
||||
// work without a wrapping struct. low64 already carries the well-mixed half of
|
||||
|
||||
@@ -170,17 +170,19 @@ mVUop(mVU_RSQRT)
|
||||
a64::Label fsNotZero2;
|
||||
armAsm->B(&fsNotZero2, a64::ne);
|
||||
|
||||
// 0/0 => Invalid
|
||||
armAsm->Mov(gprT1.W(), divI);
|
||||
mVUstrField(mVU, gprT1, &mVU.divFlag);
|
||||
// 0/0 => Invalid, anything else over zero => divide by zero. OR rather
|
||||
// than assign: the sign test above set I and it has to survive this.
|
||||
armAsm->Mov(gprT2.W(), divI);
|
||||
a64::Label afterFlag2;
|
||||
armAsm->B(&afterFlag2);
|
||||
|
||||
armAsm->Bind(&fsNotZero2);
|
||||
armAsm->Mov(gprT1.W(), divD);
|
||||
mVUstrField(mVU, gprT1, &mVU.divFlag);
|
||||
armAsm->Mov(gprT2.W(), divD);
|
||||
|
||||
armAsm->Bind(&afterFlag2);
|
||||
mVUldrField(mVU, gprT1, &mVU.divFlag);
|
||||
armAsm->Orr(gprT1.W(), gprT1.W(), gprT2.W());
|
||||
mVUstrField(mVU, gprT1, &mVU.divFlag);
|
||||
// Result = sign(Fs) | fmax
|
||||
armAsm->Ldr(t1, mVUglobMem(&mVUglob.signbit[0]));
|
||||
armAsm->And(Fs.V16B(), Fs.V16B(), t1.V16B());
|
||||
|
||||
@@ -11,59 +11,68 @@
|
||||
// NEON Arithmetic Functions
|
||||
//------------------------------------------------------------------
|
||||
|
||||
static void NEON_ADDPS(mV, const a64::VRegister& to, const a64::VRegister& from)
|
||||
// An operand the caller already left inside +/-fMax, so its mVUclamp3 is
|
||||
// dropped. MADD/MSUB and the A-forms set this for the product, which
|
||||
// mVUclamp4 clamped to the same bounds one emitter call earlier.
|
||||
enum
|
||||
{
|
||||
mVUclamp3(mVU, to, RQSCRATCH3, _X_Y_Z_W);
|
||||
mVUclamp3(mVU, from, RQSCRATCH3, _X_Y_Z_W);
|
||||
preClampTo = 1,
|
||||
preClampFrom = 2,
|
||||
};
|
||||
|
||||
static void NEON_ADDPS(mV, const a64::VRegister& to, const a64::VRegister& from, int preClamped = 0)
|
||||
{
|
||||
if (!(preClamped & preClampTo)) mVUclamp3(mVU, to, RQSCRATCH3, _X_Y_Z_W);
|
||||
if (!(preClamped & preClampFrom)) mVUclamp3(mVU, from, RQSCRATCH3, _X_Y_Z_W);
|
||||
armAsm->Fadd(to.V4S(), to.V4S(), from.V4S());
|
||||
mVUclamp4(mVU, to, RQSCRATCH3, _X_Y_Z_W);
|
||||
}
|
||||
|
||||
static void NEON_SUBPS(mV, const a64::VRegister& to, const a64::VRegister& from)
|
||||
static void NEON_SUBPS(mV, const a64::VRegister& to, const a64::VRegister& from, int preClamped = 0)
|
||||
{
|
||||
mVUclamp3(mVU, to, RQSCRATCH3, _X_Y_Z_W);
|
||||
mVUclamp3(mVU, from, RQSCRATCH3, _X_Y_Z_W);
|
||||
if (!(preClamped & preClampTo)) mVUclamp3(mVU, to, RQSCRATCH3, _X_Y_Z_W);
|
||||
if (!(preClamped & preClampFrom)) mVUclamp3(mVU, from, RQSCRATCH3, _X_Y_Z_W);
|
||||
armAsm->Fsub(to.V4S(), to.V4S(), from.V4S());
|
||||
mVUclamp4(mVU, to, RQSCRATCH3, _X_Y_Z_W);
|
||||
}
|
||||
|
||||
static void NEON_MULPS(mV, const a64::VRegister& to, const a64::VRegister& from)
|
||||
static void NEON_MULPS(mV, const a64::VRegister& to, const a64::VRegister& from, int preClamped = 0)
|
||||
{
|
||||
mVUclamp3(mVU, to, RQSCRATCH3, _X_Y_Z_W);
|
||||
mVUclamp3(mVU, from, RQSCRATCH3, _X_Y_Z_W);
|
||||
if (!(preClamped & preClampTo)) mVUclamp3(mVU, to, RQSCRATCH3, _X_Y_Z_W);
|
||||
if (!(preClamped & preClampFrom)) mVUclamp3(mVU, from, RQSCRATCH3, _X_Y_Z_W);
|
||||
armAsm->Fmul(to.V4S(), to.V4S(), from.V4S());
|
||||
mVUclamp4(mVU, to, RQSCRATCH3, _X_Y_Z_W);
|
||||
}
|
||||
|
||||
static void NEON_ADDSS(mV, const a64::VRegister& to, const a64::VRegister& from)
|
||||
static void NEON_ADDSS(mV, const a64::VRegister& to, const a64::VRegister& from, int preClamped = 0)
|
||||
{
|
||||
mVUclamp3(mVU, to, RQSCRATCH3, 0x8);
|
||||
mVUclamp3(mVU, from, RQSCRATCH3, 0x8);
|
||||
if (!(preClamped & preClampTo)) mVUclamp3(mVU, to, RQSCRATCH3, 0x8);
|
||||
if (!(preClamped & preClampFrom)) mVUclamp3(mVU, from, RQSCRATCH3, 0x8);
|
||||
armAsm->Fadd(a64::SRegister(to.GetCode()), a64::SRegister(to.GetCode()), a64::SRegister(from.GetCode()));
|
||||
mVUclamp4(mVU, to, RQSCRATCH3, 0x8);
|
||||
}
|
||||
|
||||
static void NEON_SUBSS(mV, const a64::VRegister& to, const a64::VRegister& from)
|
||||
static void NEON_SUBSS(mV, const a64::VRegister& to, const a64::VRegister& from, int preClamped = 0)
|
||||
{
|
||||
mVUclamp3(mVU, to, RQSCRATCH3, 0x8);
|
||||
mVUclamp3(mVU, from, RQSCRATCH3, 0x8);
|
||||
if (!(preClamped & preClampTo)) mVUclamp3(mVU, to, RQSCRATCH3, 0x8);
|
||||
if (!(preClamped & preClampFrom)) mVUclamp3(mVU, from, RQSCRATCH3, 0x8);
|
||||
armAsm->Fsub(a64::SRegister(to.GetCode()), a64::SRegister(to.GetCode()), a64::SRegister(from.GetCode()));
|
||||
mVUclamp4(mVU, to, RQSCRATCH3, 0x8);
|
||||
}
|
||||
|
||||
static void NEON_MULSS(mV, const a64::VRegister& to, const a64::VRegister& from)
|
||||
static void NEON_MULSS(mV, const a64::VRegister& to, const a64::VRegister& from, int preClamped = 0)
|
||||
{
|
||||
mVUclamp3(mVU, to, RQSCRATCH3, 0x8);
|
||||
mVUclamp3(mVU, from, RQSCRATCH3, 0x8);
|
||||
if (!(preClamped & preClampTo)) mVUclamp3(mVU, to, RQSCRATCH3, 0x8);
|
||||
if (!(preClamped & preClampFrom)) mVUclamp3(mVU, from, RQSCRATCH3, 0x8);
|
||||
armAsm->Fmul(a64::SRegister(to.GetCode()), a64::SRegister(to.GetCode()), a64::SRegister(from.GetCode()));
|
||||
mVUclamp4(mVU, to, RQSCRATCH3, 0x8);
|
||||
}
|
||||
|
||||
// ADD2 variants — ADDi (opType 5). The PS form needs no special handling; the
|
||||
// SS form implements the tri-ace VuAddSubHack when the gamefix is enabled.
|
||||
static void NEON_ADD2PS(mV, const a64::VRegister& to, const a64::VRegister& from)
|
||||
static void NEON_ADD2PS(mV, const a64::VRegister& to, const a64::VRegister& from, int preClamped = 0)
|
||||
{
|
||||
NEON_ADDPS(mVU, to, from);
|
||||
NEON_ADDPS(mVU, to, from, preClamped);
|
||||
}
|
||||
|
||||
// Port of x86 ADD_SS_TriAceHack (microVU_Misc.inl). Tri-ace games need ADDi to be
|
||||
@@ -71,7 +80,7 @@ static void NEON_ADD2PS(mV, const a64::VRegister& to, const a64::VRegister& from
|
||||
// flushed to a signed zero (sign bit kept, exponent+mantissa of lane 0 cleared —
|
||||
// the x86 PAND against {0x80000000, ~0, ~0, ~0}) before the scalar add. Unclamped,
|
||||
// matching x86. Without the gamefix this is a plain scalar add.
|
||||
static void NEON_ADD2SS(mV, const a64::VRegister& to, const a64::VRegister& from)
|
||||
static void NEON_ADD2SS(mV, const a64::VRegister& to, const a64::VRegister& from, int /*preClamped*/ = 0)
|
||||
{
|
||||
if (!CHECK_VUADDSUBHACK)
|
||||
{
|
||||
@@ -116,7 +125,7 @@ static void NEON_ADD2SS(mV, const a64::VRegister& to, const a64::VRegister& from
|
||||
// For each lane: t = (val >> 31) ? (val ^ 0x7fffffff) : val
|
||||
// Then CMGT.4S selects the correct operand.
|
||||
|
||||
static void NEON_MAXPS(mV, const a64::VRegister& to, const a64::VRegister& from)
|
||||
static void NEON_MAXPS(mV, const a64::VRegister& to, const a64::VRegister& from, int /*preClamped*/ = 0)
|
||||
{
|
||||
const a64::VRegister& t1 = mVU.regAlloc->allocReg();
|
||||
const a64::VRegister& t2 = mVU.regAlloc->allocReg();
|
||||
@@ -140,7 +149,7 @@ static void NEON_MAXPS(mV, const a64::VRegister& to, const a64::VRegister& from)
|
||||
mVU.regAlloc->clearNeeded(t2);
|
||||
}
|
||||
|
||||
static void NEON_MINPS(mV, const a64::VRegister& to, const a64::VRegister& from)
|
||||
static void NEON_MINPS(mV, const a64::VRegister& to, const a64::VRegister& from, int /*preClamped*/ = 0)
|
||||
{
|
||||
const a64::VRegister& t1 = mVU.regAlloc->allocReg();
|
||||
const a64::VRegister& t2 = mVU.regAlloc->allocReg();
|
||||
@@ -164,7 +173,7 @@ static void NEON_MINPS(mV, const a64::VRegister& to, const a64::VRegister& from)
|
||||
mVU.regAlloc->clearNeeded(t2);
|
||||
}
|
||||
|
||||
static void NEON_MAXSS(mV, const a64::VRegister& to, const a64::VRegister& from)
|
||||
static void NEON_MAXSS(mV, const a64::VRegister& to, const a64::VRegister& from, int /*preClamped*/ = 0)
|
||||
{
|
||||
const a64::VRegister& t1 = mVU.regAlloc->allocReg();
|
||||
|
||||
@@ -189,7 +198,7 @@ static void NEON_MAXSS(mV, const a64::VRegister& to, const a64::VRegister& from)
|
||||
mVU.regAlloc->clearNeeded(t1);
|
||||
}
|
||||
|
||||
static void NEON_MINSS(mV, const a64::VRegister& to, const a64::VRegister& from)
|
||||
static void NEON_MINSS(mV, const a64::VRegister& to, const a64::VRegister& from, int /*preClamped*/ = 0)
|
||||
{
|
||||
const a64::VRegister& t1 = mVU.regAlloc->allocReg();
|
||||
|
||||
@@ -217,7 +226,7 @@ static void NEON_MINSS(mV, const a64::VRegister& to, const a64::VRegister& from)
|
||||
//------------------------------------------------------------------
|
||||
// opType: 0=ADD, 1=SUB, 2=MUL, 3=MAX, 4=MIN, 5=ADD2
|
||||
|
||||
typedef void (*NEONarithPS)(microVU&, const a64::VRegister&, const a64::VRegister&);
|
||||
typedef void (*NEONarithPS)(microVU&, const a64::VRegister&, const a64::VRegister&, int);
|
||||
|
||||
static NEONarithPS const NEON_PS[] = {
|
||||
NEON_ADDPS, // 0
|
||||
@@ -428,8 +437,8 @@ static void mVU_FMACa(microVU& mVU, int recPass, int opCase, int opType, bool is
|
||||
// vm's scalar format, and a V4S vm silently selects the half-precision
|
||||
// opcode once Devel strips the VIXL_ASSERT.
|
||||
if (bcLane >= 0) armAsm->Fmul(Fs.V4S(), Fs.V4S(), Ft.S(), bcLane);
|
||||
else if (_XYZW_SS) NEON_SS[opType](mVU, Fs, Ft);
|
||||
else NEON_PS[opType](mVU, Fs, Ft);
|
||||
else if (_XYZW_SS) NEON_SS[opType](mVU, Fs, Ft, 0);
|
||||
else NEON_PS[opType](mVU, Fs, Ft, 0);
|
||||
|
||||
if (isACC)
|
||||
{
|
||||
@@ -485,10 +494,14 @@ static void mVU_FMACb(microVU& mVU, int recPass, int opCase, int opType, microOp
|
||||
if ((clampType & cFt) && bcLane < 0) mVUclamp2(mVU, Ft, a64::NoVReg, _X_Y_Z_W);
|
||||
if (clampType & cFs) mVUclamp2(mVU, Fs, a64::NoVReg, _X_Y_Z_W);
|
||||
|
||||
// mVUclamp4 does not run behind the AX-14 lane fold, which replaces
|
||||
// NEON_*[2], nor in the sign-preserving mode, which clamps operands only.
|
||||
const bool prodClamped = (bcLane < 0) && !CHECK_VU_SIGN_OVERFLOW(mVU.index);
|
||||
|
||||
// Step 1: Multiply Fs * Ft
|
||||
if (bcLane >= 0) armAsm->Fmul(Fs.V4S(), Fs.V4S(), Ft.S(), bcLane); // AX-14 fold
|
||||
else if (_XYZW_SS) NEON_SS[2](mVU, Fs, Ft);
|
||||
else NEON_PS[2](mVU, Fs, Ft);
|
||||
else if (_XYZW_SS) NEON_SS[2](mVU, Fs, Ft, 0);
|
||||
else NEON_PS[2](mVU, Fs, Ft, 0);
|
||||
|
||||
// Step 2: ADD/SUB the product to/from ACC
|
||||
if (_XYZW_SS || _X_Y_Z_W == 0xf)
|
||||
@@ -503,13 +516,13 @@ static void mVU_FMACb(microVU& mVU, int recPass, int opCase, int opType, microOp
|
||||
// mirroring the load+Ins pattern mVU_FMACa uses for its ACC.
|
||||
const a64::VRegister& accSS = mVU.regAlloc->allocReg();
|
||||
armAsm->Mov(accSS.V16B(), ACC.V16B());
|
||||
NEON_SS[opType](mVU, accSS, Fs);
|
||||
NEON_SS[opType](mVU, accSS, Fs, prodClamped ? preClampFrom : 0);
|
||||
armAsm->Ins(ACC.V4S(), 0, accSS.V4S(), 0);
|
||||
mVU.regAlloc->clearNeeded(accSS);
|
||||
}
|
||||
else
|
||||
{
|
||||
NEON_PS[opType](mVU, ACC, Fs);
|
||||
NEON_PS[opType](mVU, ACC, Fs, prodClamped ? preClampFrom : 0);
|
||||
}
|
||||
mVUupdateFlags(mVU, ACC, Fs, tempFt);
|
||||
if (_XYZW_SS && _X_Y_Z_W != 8)
|
||||
@@ -519,7 +532,7 @@ static void mVU_FMACb(microVU& mVU, int recPass, int opCase, int opType, microOp
|
||||
{
|
||||
const a64::VRegister& tempACC = mVU.regAlloc->allocReg();
|
||||
armAsm->Mov(tempACC.V16B(), ACC.V16B());
|
||||
NEON_PS[opType](mVU, tempACC, Fs);
|
||||
NEON_PS[opType](mVU, tempACC, Fs, prodClamped ? preClampFrom : 0);
|
||||
mVUmergeRegs(ACC, tempACC, _X_Y_Z_W);
|
||||
mVUupdateFlags(mVU, ACC, Fs, tempFt);
|
||||
mVU.regAlloc->clearNeeded(tempACC);
|
||||
@@ -562,14 +575,16 @@ static void mVU_FMACc(microVU& mVU, int recPass, int opCase, microOpcode opEnum,
|
||||
if (clampType & cFs) mVUclamp2(mVU, Fs, a64::NoVReg, _X_Y_Z_W);
|
||||
if (clampType & cACC) mVUclamp2(mVU, ACC, a64::NoVReg, _X_Y_Z_W);
|
||||
|
||||
const bool prodClamped = (bcLane < 0) && !CHECK_VU_SIGN_OVERFLOW(mVU.index);
|
||||
|
||||
// Step 1: Fs = Fs * Ft
|
||||
// Step 2: Fs = Fs + ACC
|
||||
if (_XYZW_SS) { NEON_SS[2](mVU, Fs, Ft); NEON_SS[0](mVU, Fs, ACC); }
|
||||
if (_XYZW_SS) { NEON_SS[2](mVU, Fs, Ft, 0); NEON_SS[0](mVU, Fs, ACC, prodClamped ? preClampTo : 0); }
|
||||
else
|
||||
{
|
||||
if (bcLane >= 0) armAsm->Fmul(Fs.V4S(), Fs.V4S(), Ft.S(), bcLane); // AX-14 fold
|
||||
else NEON_PS[2](mVU, Fs, Ft);
|
||||
NEON_PS[0](mVU, Fs, ACC);
|
||||
else NEON_PS[2](mVU, Fs, Ft, 0);
|
||||
NEON_PS[0](mVU, Fs, ACC, prodClamped ? preClampTo : 0);
|
||||
}
|
||||
|
||||
if (_XYZW_SS2)
|
||||
@@ -610,14 +625,16 @@ static void mVU_FMACd(microVU& mVU, int recPass, int opCase, microOpcode opEnum,
|
||||
if (clampType & cFs) mVUclamp2(mVU, Fs, a64::NoVReg, _X_Y_Z_W);
|
||||
if (clampType & cACC) mVUclamp2(mVU, Fd, a64::NoVReg, _X_Y_Z_W);
|
||||
|
||||
const bool prodClamped = (bcLane < 0) && !CHECK_VU_SIGN_OVERFLOW(mVU.index);
|
||||
|
||||
// Step 1: Fs = Fs * Ft
|
||||
// Step 2: Fd = Fd - Fs (Fd starts as ACC)
|
||||
if (_XYZW_SS) { NEON_SS[2](mVU, Fs, Ft); NEON_SS[1](mVU, Fd, Fs); }
|
||||
if (_XYZW_SS) { NEON_SS[2](mVU, Fs, Ft, 0); NEON_SS[1](mVU, Fd, Fs, prodClamped ? preClampFrom : 0); }
|
||||
else
|
||||
{
|
||||
if (bcLane >= 0) armAsm->Fmul(Fs.V4S(), Fs.V4S(), Ft.S(), bcLane); // AX-14 fold
|
||||
else NEON_PS[2](mVU, Fs, Ft);
|
||||
NEON_PS[1](mVU, Fd, Fs);
|
||||
else NEON_PS[2](mVU, Fs, Ft, 0);
|
||||
NEON_PS[1](mVU, Fd, Fs, prodClamped ? preClampFrom : 0);
|
||||
}
|
||||
|
||||
mVUupdateFlags(mVU, Fd, Fs, tempFt);
|
||||
|
||||
@@ -47,6 +47,7 @@ add_pcsx2_test(recompiler_tests
|
||||
ee_rec_callret_tests.cpp
|
||||
ee_rec_cop0_tests.cpp
|
||||
ee_rec_fpu_tests.cpp
|
||||
ee_rec_fpu_divunit_rounding_tests.cpp
|
||||
ee_rec_fpu_full_mode_tests.cpp
|
||||
ee_rec_fpu_guardbit_tests.cpp
|
||||
ee_rec_fpu_rsqrt_tests.cpp
|
||||
@@ -128,10 +129,25 @@ add_pcsx2_test(recompiler_tests
|
||||
ee_fpu_overflow_console_conformance_tests.cpp
|
||||
ee_fpu_absneg_clamp_tests.cpp
|
||||
ee_fpu_minmax_console_tests.cpp
|
||||
ee_fpu_cascade_console_tests.cpp
|
||||
ee_fpu_compare_console_tests.cpp
|
||||
ee_fpu_divunit_console_tests.cpp
|
||||
ee_fpu_divunit_exhaustive_tests.cpp
|
||||
ee_fpu_guarded_addsub_console_tests.cpp
|
||||
ee_fpu_underflow_console_tests.cpp
|
||||
ee_fpu_top_binade_console_tests.cpp
|
||||
ee_fpu_zero_divisor_console_tests.cpp
|
||||
ee_fpu_rsqrt_sign_console_tests.cpp
|
||||
ee_lsu_console_conformance_tests.cpp
|
||||
vu0_macro_console_conformance_tests.cpp
|
||||
vu_madd_contract_console_tests.cpp
|
||||
vu1_efu_console_conformance_tests.cpp
|
||||
vu_sticky_console_conformance_tests.cpp
|
||||
vu_rsqrt_divisor_sign_tests.cpp
|
||||
vu_divunit_console_conformance_tests.cpp
|
||||
vu_branch_console_conformance_tests.cpp
|
||||
vu_pipeline_console_conformance_tests.cpp
|
||||
vu_memory_xgkick_console_conformance_tests.cpp
|
||||
ee_sa_perf_console_conformance_tests.cpp
|
||||
ee_cache_console_conformance_tests.cpp
|
||||
ee_cache2_console_conformance_tests.cpp
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user