mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-09-11 15:13:37 +02:00
Interpreter: honor the guest's FPU rounding mode and flush-to-zero
Every JIT backend puts the host FPU into the mode fcr31 asks for (bits 0-1 and 24) before running emulated code, and takes it back out before calling any host code. The plain interpreter did none of that, so all its float math rounded to nearest with denormals intact no matter what the game had set - cpu/fpu/fpu fails under -i and passes under the JIT on exactly this. Move the helpers the IR interpreter already had for this out of IRInterpreter and into MIPS.cpp as ApplyHostRoundingMode/RestoreHostRoundingMode, and use them around the interpreter's run loop and single step, restoring around syscalls and replacement functions, which are host code. ctc1 re-applies immediately, since the interpreter has no block boundary to defer it to. round.w.s changes with it: it was floorf(x + 0.5f), which is half-away-from-zero rather than the half-to-even every JIT produces, and the add would now pick up the guest's rounding mode on top of that. round_ieee_754 is both correct and mode-independent, and is what cvt.w.s already used for the same rounding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SfY7iFJEjmRXf1XGrTs4MF
This commit is contained in:
co-authored by
Claude Opus 5
parent
adccd302e5
commit
3fa67f22ed
@@ -3,10 +3,6 @@
|
||||
|
||||
#include "ppsspp_config.h"
|
||||
|
||||
#if PPSSPP_PLATFORM(WINDOWS) && PPSSPP_ARCH(ARM64)
|
||||
#include <arm64intr.h>
|
||||
#endif
|
||||
|
||||
#include "Common/BitSet.h"
|
||||
#include "Common/BitScan.h"
|
||||
#include "Common/Common.h"
|
||||
@@ -28,32 +24,6 @@
|
||||
#include "Core/System.h"
|
||||
#include "Core/MIPS/MIPSTracer.h"
|
||||
|
||||
#if PPSSPP_ARCH(ARM64)
|
||||
|
||||
// TODO: This should be put in some common header.
|
||||
static inline u64 ARM64ReadFPCR() {
|
||||
#if PPSSPP_PLATFORM(WINDOWS)
|
||||
return _ReadStatusReg(ARM64_FPCR);
|
||||
#else
|
||||
// TODO: Try __builtin_arm_get_fpcr()
|
||||
u64 fpcr; // not really 64-bit, just to match the register size.
|
||||
asm volatile ("mrs %0, fpcr" : "=r" (fpcr));
|
||||
return fpcr;
|
||||
#endif
|
||||
}
|
||||
|
||||
static inline void ARM64WriteFPCR(u64 fpcr) {
|
||||
#if PPSSPP_PLATFORM(WINDOWS)
|
||||
_WriteStatusReg(ARM64_FPCR, fpcr);
|
||||
#else
|
||||
// TODO: Try __builtin_arm_set_fpcr()
|
||||
// Write back the modified FPCR
|
||||
asm volatile ("msr fpcr, %0" : : "r" (fpcr));
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef mips
|
||||
// Why do MIPS compilers define something so generic? Try to keep defined, at least...
|
||||
#undef mips
|
||||
@@ -110,57 +80,6 @@ u32 IRRunMemCheck(u32 pc, u32 addr) {
|
||||
return coreState != CORE_RUNNING_CPU ? 1 : 0;
|
||||
}
|
||||
|
||||
void IRApplyRounding(MIPSState *mips) {
|
||||
u32 fcr1Bits = mips->fcr31 & 0x01000003;
|
||||
// If these are 0, we just leave things as they are.
|
||||
if (fcr1Bits) {
|
||||
int rmode = fcr1Bits & 3;
|
||||
bool ftz = (fcr1Bits & 0x01000000) != 0;
|
||||
#if PPSSPP_ARCH(SSE2)
|
||||
u32 csr = _mm_getcsr() & ~0x6000;
|
||||
// Translate the rounding mode bits to X86, the same way as in Asm.cpp.
|
||||
if (rmode & 1) {
|
||||
rmode ^= 2;
|
||||
}
|
||||
csr |= rmode << 13;
|
||||
|
||||
if (ftz) {
|
||||
// Flush to zero
|
||||
csr |= 0x8000;
|
||||
}
|
||||
_mm_setcsr(csr);
|
||||
#elif PPSSPP_ARCH(ARM64)
|
||||
u64 fpcr = ARM64ReadFPCR();
|
||||
// Translate MIPS to ARM rounding mode
|
||||
static const u8 lookup[4] = {0, 3, 1, 2};
|
||||
|
||||
fpcr &= ~(3 << 22); // Clear bits [23:22]
|
||||
fpcr |= ((u64)lookup[rmode] << 22);
|
||||
|
||||
if (ftz) {
|
||||
fpcr |= 1 << 24;
|
||||
}
|
||||
|
||||
ARM64WriteFPCR(fpcr);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void IRRestoreRounding() {
|
||||
#if PPSSPP_ARCH(SSE2)
|
||||
// TODO: We should avoid this if we didn't apply rounding in the first place.
|
||||
// In the meantime, clear out FTZ and rounding mode bits.
|
||||
u32 csr = _mm_getcsr();
|
||||
csr &= ~(7 << 13);
|
||||
_mm_setcsr(csr);
|
||||
#elif PPSSPP_ARCH(ARM64)
|
||||
u64 fpcr = ARM64ReadFPCR(); // not really 64-bit, just to match the regsiter size.
|
||||
fpcr &= ~(7 << 22); // Clear bits [23:22] for rounding, 24 for FTZ
|
||||
// Write back the modified FPCR
|
||||
ARM64WriteFPCR(fpcr);
|
||||
#endif
|
||||
}
|
||||
|
||||
u32 IRInterpret(MIPSState *mips, const IRInst *inst) {
|
||||
while (true) {
|
||||
switch (inst->op) {
|
||||
@@ -1257,10 +1176,10 @@ u32 IRInterpret(MIPSState *mips, const IRInst *inst) {
|
||||
break;
|
||||
|
||||
case IROp::ApplyRoundingMode:
|
||||
IRApplyRounding(mips);
|
||||
ApplyHostRoundingMode(mips);
|
||||
break;
|
||||
case IROp::RestoreRoundingMode:
|
||||
IRRestoreRounding();
|
||||
RestoreHostRoundingMode();
|
||||
break;
|
||||
case IROp::UpdateRoundingMode:
|
||||
// TODO: Implement
|
||||
|
||||
@@ -11,9 +11,6 @@ u32 IRRunBreakpoint(u32 pc);
|
||||
u32 IRRunMemCheck(u32 pc, u32 addr);
|
||||
u32 IRInterpret(MIPSState *ms, const IRInst *inst);
|
||||
|
||||
void IRApplyRounding();
|
||||
void IRRestoreRounding();
|
||||
|
||||
template <uint32_t alignment>
|
||||
u32 RunValidateAddress(u32 pc, u32 addr, u32 isWrite) {
|
||||
static_assert(alignment <= 16);
|
||||
|
||||
@@ -82,6 +82,8 @@ int MIPS_InterpretSingleStep(MIPSState *mips) {
|
||||
return 0;
|
||||
}
|
||||
MIPSOpcode op = Memory::Read_Opcode_JIT(mips->pc); // now unchecked
|
||||
// Same reason as the run loop in MIPSInterpret_RunUntil - see ApplyHostRoundingMode.
|
||||
ApplyHostRoundingMode(mips);
|
||||
if (mips->inDelaySlot) {
|
||||
MIPSInterpret(mips, op);
|
||||
if (mips->inDelaySlot) {
|
||||
@@ -91,6 +93,7 @@ int MIPS_InterpretSingleStep(MIPSState *mips) {
|
||||
} else {
|
||||
MIPSInterpret(mips, op);
|
||||
}
|
||||
RestoreHostRoundingMode();
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -235,7 +238,10 @@ namespace MIPSInt {
|
||||
mips->pc += 4;
|
||||
}
|
||||
mips->inDelaySlot = false;
|
||||
// HLE code is host code - it must not run under the guest's rounding mode.
|
||||
RestoreHostRoundingMode();
|
||||
CallSyscallWithPC(op, syscallPC);
|
||||
ApplyHostRoundingMode(mips);
|
||||
}
|
||||
|
||||
void Int_Sync(MIPSState *mips, MIPSOpcode op) {
|
||||
@@ -718,6 +724,13 @@ namespace MIPSInt {
|
||||
if (MIPSComp::jit) {
|
||||
// In case of DISABLE, we need to tell jit we updated FCR31.
|
||||
MIPSComp::jit->UpdateFCR31();
|
||||
} else {
|
||||
// The interpreter emulates the rounding mode by putting the host FPU in it,
|
||||
// so it has to switch right here rather than at the next block boundary.
|
||||
// Restore first: the new value may be back to the default, which Apply
|
||||
// deliberately doesn't write.
|
||||
RestoreHostRoundingMode();
|
||||
ApplyHostRoundingMode(mips);
|
||||
}
|
||||
} else {
|
||||
WARN_LOG_REPORT(Log::CPU, "WriteFCR: Unexpected reg %d (value %08x)", fs, value);
|
||||
@@ -1100,7 +1113,9 @@ namespace MIPSInt {
|
||||
}
|
||||
switch (op & 0x3f)
|
||||
{
|
||||
case 12: FsI(fd) = (int)floorf(F(fs)+0.5f); break; //round.w.s
|
||||
// round.w.s is round-half-to-even, not half-away-from-zero - and its mode is fixed,
|
||||
// so unlike cvt.w.s below it must not follow fcr31. round_ieee_754 is both.
|
||||
case 12: FsI(fd) = (int)round_ieee_754(F(fs)); break; //round.w.s
|
||||
case 13: //trunc.w.s
|
||||
if (F(fs) >= 0.0f) {
|
||||
FsI(fd) = (int)floorf(F(fs));
|
||||
@@ -1247,7 +1262,10 @@ namespace MIPSInt {
|
||||
int index = op.encoding & 0xFFFFFF;
|
||||
const ReplacementTableEntry *entry = GetReplacementFunc(index);
|
||||
if (entry && entry->replaceFunc && (entry->flags & REPFLAG_DISABLED) == 0) {
|
||||
// Like a syscall, a replacement function is host code - see Int_Syscall.
|
||||
RestoreHostRoundingMode();
|
||||
int cycles = entry->replaceFunc();
|
||||
ApplyHostRoundingMode(mips);
|
||||
|
||||
if (entry->flags & (REPFLAG_HOOKENTER | REPFLAG_HOOKEXIT)) {
|
||||
// Interpret the original instruction under the hook.
|
||||
|
||||
@@ -20,8 +20,14 @@
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
|
||||
#include "ppsspp_config.h"
|
||||
|
||||
#if PPSSPP_PLATFORM(WINDOWS) && PPSSPP_ARCH(ARM64)
|
||||
#include <arm64intr.h>
|
||||
#endif
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/Math/SIMDHeaders.h"
|
||||
#include "Common/Serialize/Serializer.h"
|
||||
#include "Common/Serialize/SerializeFuncs.h"
|
||||
#include "Core/ConfigValues.h"
|
||||
@@ -42,6 +48,82 @@ MIPSState *currentMIPS = &mipsr4k;
|
||||
MIPSDebugInterface debugr4k(&mipsr4k);
|
||||
MIPSDebugInterface *currentDebugMIPS = &debugr4k;
|
||||
|
||||
#if PPSSPP_ARCH(ARM64)
|
||||
|
||||
static inline u64 ARM64ReadFPCR() {
|
||||
#if PPSSPP_PLATFORM(WINDOWS)
|
||||
return _ReadStatusReg(ARM64_FPCR);
|
||||
#else
|
||||
// TODO: Try __builtin_arm_get_fpcr()
|
||||
u64 fpcr; // not really 64-bit, just to match the register size.
|
||||
asm volatile ("mrs %0, fpcr" : "=r" (fpcr));
|
||||
return fpcr;
|
||||
#endif
|
||||
}
|
||||
|
||||
static inline void ARM64WriteFPCR(u64 fpcr) {
|
||||
#if PPSSPP_PLATFORM(WINDOWS)
|
||||
_WriteStatusReg(ARM64_FPCR, fpcr);
|
||||
#else
|
||||
// TODO: Try __builtin_arm_set_fpcr()
|
||||
// Write back the modified FPCR
|
||||
asm volatile ("msr fpcr, %0" : : "r" (fpcr));
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void ApplyHostRoundingMode(const MIPSState *mips) {
|
||||
u32 fcr1Bits = mips->fcr31 & 0x01000003;
|
||||
// If these are 0, we just leave things as they are.
|
||||
if (fcr1Bits) {
|
||||
int rmode = fcr1Bits & 3;
|
||||
bool ftz = (fcr1Bits & 0x01000000) != 0;
|
||||
#if PPSSPP_ARCH(SSE2)
|
||||
u32 csr = _mm_getcsr() & ~0x6000;
|
||||
// Translate the rounding mode bits to X86, the same way as in Asm.cpp.
|
||||
if (rmode & 1) {
|
||||
rmode ^= 2;
|
||||
}
|
||||
csr |= rmode << 13;
|
||||
|
||||
if (ftz) {
|
||||
// Flush to zero
|
||||
csr |= 0x8000;
|
||||
}
|
||||
_mm_setcsr(csr);
|
||||
#elif PPSSPP_ARCH(ARM64)
|
||||
u64 fpcr = ARM64ReadFPCR();
|
||||
// Translate MIPS to ARM rounding mode
|
||||
static const u8 lookup[4] = {0, 3, 1, 2};
|
||||
|
||||
fpcr &= ~(3 << 22); // Clear bits [23:22]
|
||||
fpcr |= ((u64)lookup[rmode] << 22);
|
||||
|
||||
if (ftz) {
|
||||
fpcr |= 1 << 24;
|
||||
}
|
||||
|
||||
ARM64WriteFPCR(fpcr);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void RestoreHostRoundingMode() {
|
||||
// TODO: We should avoid this if we didn't apply rounding in the first place.
|
||||
// In the meantime, clear out FTZ and rounding mode bits.
|
||||
#if PPSSPP_ARCH(SSE2)
|
||||
u32 csr = _mm_getcsr();
|
||||
csr &= ~(7 << 13);
|
||||
_mm_setcsr(csr);
|
||||
#elif PPSSPP_ARCH(ARM64)
|
||||
u64 fpcr = ARM64ReadFPCR(); // not really 64-bit, just to match the register size.
|
||||
fpcr &= ~(7 << 22); // Clear bits [23:22] for rounding, 24 for FTZ
|
||||
// Write back the modified FPCR
|
||||
ARM64WriteFPCR(fpcr);
|
||||
#endif
|
||||
}
|
||||
|
||||
u8 voffset[128];
|
||||
u8 fromvoffset[128];
|
||||
|
||||
|
||||
@@ -293,3 +293,12 @@ extern MIPSDebugInterface *currentDebugMIPS;
|
||||
extern MIPSState mipsr4k;
|
||||
|
||||
extern const float cst_constants[32];
|
||||
|
||||
// The guest's rounding mode and flush-to-zero flag (fcr31 bits 0-1 and 24) are emulated by putting
|
||||
// the *host* FPU into the matching mode, since we do the arithmetic with plain host float ops.
|
||||
// That mode must not be left on while running anything that isn't emulating a guest instruction -
|
||||
// HLE syscalls, replacement functions, the GPU - so an emulation loop applies it on entry and
|
||||
// restores it before calling out, the way the JITs do (see Jit::ApplyRoundingMode).
|
||||
// Apply is cheap when the guest is in the default mode (by far the common case): it does nothing.
|
||||
void ApplyHostRoundingMode(const MIPSState *mips);
|
||||
void RestoreHostRoundingMode();
|
||||
|
||||
@@ -1272,6 +1272,12 @@ int MIPSInterpret_RunUntil(MIPSState *mips, u64 globalTicks) {
|
||||
while (coreState == CORE_RUNNING_CPU) {
|
||||
CoreTiming::Advance(mips);
|
||||
|
||||
// The emulated instructions below do their float math with plain host float ops, so the
|
||||
// host FPU has to be in the guest's rounding mode while they run - and back in the normal
|
||||
// one whenever we're not running them, which is what the JITs do too. Calls out from
|
||||
// inside the run loops (syscalls, replacement functions) restore it themselves.
|
||||
ApplyHostRoundingMode(mips);
|
||||
|
||||
uint64_t ticksLeft = globalTicks - CoreTiming::GetTicks(mips);
|
||||
if (g_breakpoints.HasBreakPoints() || g_breakpoints.HasMemChecks() || g_breakpoints.HasRegBreakpoints() || ticksLeft <= mips->downcount) {
|
||||
RunUntilDowncountZeroWithChecks(mips, globalTicks);
|
||||
@@ -1279,6 +1285,8 @@ int MIPSInterpret_RunUntil(MIPSState *mips, u64 globalTicks) {
|
||||
RunUntilDowncountZeroFast(mips);
|
||||
}
|
||||
|
||||
RestoreHostRoundingMode();
|
||||
|
||||
if (CoreTiming::GetTicks(mips) > globalTicks) {
|
||||
// DEBUG_LOG(Log::CPU, "Hit the max ticks, bailing 1 : %llu, %llu", globalTicks, CoreTiming::GetTicks(mips));
|
||||
return 1;
|
||||
|
||||
Reference in New Issue
Block a user