mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
Merge yaps2/main: EE FPU MAX.S/MIN.S operand clamp at eeClampMode>=1
This commit is contained in:
@@ -419,6 +419,26 @@ static a64::VRegister fpuClampInput(const a64::VRegister& src, const a64::VRegis
|
||||
return scratch;
|
||||
}
|
||||
|
||||
// Source-operand clamp for MAX.S / MIN.S. Identical sign-preserving inf/NaN ->
|
||||
// ±fMax to fpuClampInput, but gated on CHECK_FPU_OVERFLOW (eeClampMode >= 1)
|
||||
// instead of CHECK_FPU_EXTRA_OVERFLOW (>= 2). x86 routes MAX/MIN through
|
||||
// recCommutativeOp with op>=2, whose gate `CHECK_FPU_EXTRA_OVERFLOW || (op>=2)`
|
||||
// is always true, so it fpuFloat2-clamps both operands whenever CHECK_FPU_OVERFLOW
|
||||
// — a strictly lower threshold than the arithmetic ops. AetherSX2's shipped arm64
|
||||
// rec gates the same MAX/MIN clamp on fpuOverflow (options bit 8) vs ADD/SUB's
|
||||
// bit 8+9 (verified by disassembly of the 3606 build). Without it a raw Inf/NaN
|
||||
// operand (via MOV.S/LWC1/MTC1) survives the NaN-eating Fmaxnm/Fminnm as the
|
||||
// wrong finite value — the True Crime: New York City rainbow. Off (mode 0)
|
||||
// returns the source reg and emits nothing.
|
||||
static a64::VRegister fpuClampMinMaxOperand(const a64::VRegister& src, const a64::VRegister& scratch)
|
||||
{
|
||||
if (!CHECK_FPU_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
|
||||
@@ -1080,12 +1100,18 @@ void recRSQRT_S()
|
||||
XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT);
|
||||
}
|
||||
|
||||
// PS2 FPU has no NaN concept — match x86 MAXSS/MINSS NaN-eating semantics
|
||||
// with Fmaxnm/Fminnm (Fmax/Fmin IEEE-propagate NaN, same trap as mVUclamp1).
|
||||
// No clamp needed: MAX/MIN cannot widen finite inputs.
|
||||
// PS2 FPU has no NaN concept. At eeClampMode >= 1 the operands are first
|
||||
// clamped to ±fMax (sign-preserving, via fpuClampMinMaxOperand — matching x86
|
||||
// recCommutativeOp's always-firing op>=2 clamp and AetherSX2's arm64 rec), so a
|
||||
// raw Inf/NaN operand cannot reach the max/min. The result never needs clamping
|
||||
// (max/min of two finite ±fMax-bounded inputs stays in range). Fmaxnm/Fminnm
|
||||
// (not Fmax/Fmin, which IEEE-propagate NaN) give MAXSS/MINSS NaN-eating for the
|
||||
// unclamped mode-0 case.
|
||||
static void recMAX_S_xmm(int info)
|
||||
{
|
||||
armAsm->Fmaxnm(armSRegister(EEREC_D), armSRegister(EEREC_S), armSRegister(EEREC_T));
|
||||
const a64::VRegister s = fpuClampMinMaxOperand(armSRegister(EEREC_S), RSSCRATCH);
|
||||
const a64::VRegister t = fpuClampMinMaxOperand(armSRegister(EEREC_T), RSSCRATCH2);
|
||||
armAsm->Fmaxnm(armSRegister(EEREC_D), s, t);
|
||||
}
|
||||
|
||||
void recMAX_S()
|
||||
@@ -1096,7 +1122,9 @@ void recMAX_S()
|
||||
|
||||
static void recMIN_S_xmm(int info)
|
||||
{
|
||||
armAsm->Fminnm(armSRegister(EEREC_D), armSRegister(EEREC_S), armSRegister(EEREC_T));
|
||||
const a64::VRegister s = fpuClampMinMaxOperand(armSRegister(EEREC_S), RSSCRATCH);
|
||||
const a64::VRegister t = fpuClampMinMaxOperand(armSRegister(EEREC_T), RSSCRATCH2);
|
||||
armAsm->Fminnm(armSRegister(EEREC_D), s, t);
|
||||
}
|
||||
|
||||
void recMIN_S()
|
||||
|
||||
@@ -37,6 +37,26 @@ struct FpuExtraOverflowGuard
|
||||
~FpuExtraOverflowGuard() { EmuConfig.Cpu.Recompiler.fpuExtraOverflow = saved; }
|
||||
};
|
||||
|
||||
// Scoped eeClampMode selector. Sets the two Recompiler bits SetEEClampMode
|
||||
// derives from the mode: fpuOverflow (== CHECK_FPU_OVERFLOW, mode >= 1) gates
|
||||
// the result clamp and the MAX/MIN operand clamp; fpuExtraOverflow (mode >= 2)
|
||||
// gates the arithmetic operand clamp. Restores both on scope exit.
|
||||
struct FpuClampModeGuard
|
||||
{
|
||||
bool savedO = EmuConfig.Cpu.Recompiler.fpuOverflow;
|
||||
bool savedX = EmuConfig.Cpu.Recompiler.fpuExtraOverflow;
|
||||
explicit FpuClampModeGuard(int mode)
|
||||
{
|
||||
EmuConfig.Cpu.Recompiler.fpuOverflow = (mode >= 1);
|
||||
EmuConfig.Cpu.Recompiler.fpuExtraOverflow = (mode >= 2);
|
||||
}
|
||||
~FpuClampModeGuard()
|
||||
{
|
||||
EmuConfig.Cpu.Recompiler.fpuOverflow = savedO;
|
||||
EmuConfig.Cpu.Recompiler.fpuExtraOverflow = savedX;
|
||||
}
|
||||
};
|
||||
|
||||
u32 FloatBits(float f)
|
||||
{
|
||||
u32 bits;
|
||||
@@ -860,6 +880,93 @@ TEST(EeRecFpu, SqrtSNegativeArgumentReturnsAbsRoot)
|
||||
h.ExpectFpr(3, FloatBits(5.0f));
|
||||
}
|
||||
|
||||
// ----- MAX.S / MIN.S operand clamp (eeClampMode >= 1) ----------------------
|
||||
//
|
||||
// x86 recCommutativeOp clamps MAX/MIN operands (sign-preserving inf/NaN ->
|
||||
// ±fMax via fpuFloat2) whenever CHECK_FPU_OVERFLOW — the op>=2 argument makes
|
||||
// the gate always fire, so mode >= 1, a strictly LOWER threshold than ADD/SUB's
|
||||
// mode >= 2 operand clamp. AetherSX2's shipped arm64 rec gates the identical
|
||||
// clamp on fpuOverflow (options bit 8), confirmed by disassembly. Our port
|
||||
// emitted bare Fmaxnm/Fminnm with no operand clamp, so a raw Inf/NaN FPR
|
||||
// (reachable via MOV.S/LWC1/MTC1) survived as the wrong finite value — Fmaxnm/
|
||||
// Fminnm are NaN-eating and return the *other* operand — which is the True
|
||||
// Crime: New York City rainbow (a min(max(uv,0),size) UV-clamp idiom feeding a
|
||||
// corrupt palette index). These pin the x86/AetherSX2 behavior.
|
||||
//
|
||||
// The interpreter's fp_max/fp_min run on raw bits with NO operand clamp, so the
|
||||
// (correct) JIT legitimately diverges from interp here — interp is not the
|
||||
// FPU-clamp oracle, the x86 JIT is. Hence RunJitNoDiff + GetFprBitsJit rather
|
||||
// than the auto-diffing Run()/ExpectFpr.
|
||||
TEST(EeRecFpu, MaxSClampsInfOperandAtClampMode2)
|
||||
{
|
||||
FpuClampModeGuard guard(2);
|
||||
EeRecTestHarness h;
|
||||
h.EnableCop1();
|
||||
h.SetFprBits(1, 0x7F800000u); // +Inf raw bits (poisoned fpr)
|
||||
h.SetFpr(2, 3.0f);
|
||||
h.LoadProgram({ee::MAX_S(3, 1, 2)});
|
||||
h.RunJitNoDiff();
|
||||
// clamp(+Inf) = +fMax, MAX(+fMax, 3) = +fMax. Bare Fmaxnm gives +Inf.
|
||||
EXPECT_EQ(h.GetFprBitsJit(3), 0x7F7FFFFFu);
|
||||
}
|
||||
|
||||
TEST(EeRecFpu, MaxSClampsNanOperandToPosFmax)
|
||||
{
|
||||
FpuClampModeGuard guard(2);
|
||||
EeRecTestHarness h;
|
||||
h.EnableCop1();
|
||||
h.SetFprBits(1, 0x7FC00000u); // +NaN raw bits
|
||||
h.SetFpr(2, -5.0f);
|
||||
h.LoadProgram({ee::MAX_S(3, 1, 2)});
|
||||
h.RunJitNoDiff();
|
||||
// clamp(+NaN) = +fMax, MAX(+fMax, -5) = +fMax. Bare Fmaxnm NaN-eats -> -5.0.
|
||||
EXPECT_EQ(h.GetFprBitsJit(3), 0x7F7FFFFFu);
|
||||
}
|
||||
|
||||
TEST(EeRecFpu, MinSClampsNegNanOperandToNegFmax)
|
||||
{
|
||||
FpuClampModeGuard guard(2);
|
||||
EeRecTestHarness h;
|
||||
h.EnableCop1();
|
||||
h.SetFprBits(1, 0xFFC00000u); // -NaN raw bits
|
||||
h.SetFpr(2, 5.0f);
|
||||
h.LoadProgram({ee::MIN_S(3, 1, 2)});
|
||||
h.RunJitNoDiff();
|
||||
// clamp(-NaN) = -fMax, MIN(-fMax, 5) = -fMax. Bare Fminnm NaN-eats -> 5.0.
|
||||
EXPECT_EQ(h.GetFprBitsJit(3), 0xFF7FFFFFu);
|
||||
}
|
||||
|
||||
// The gate is fpuOverflow (mode >= 1), NOT fpuExtraOverflow (mode >= 2): the
|
||||
// clamp must still fire at mode 1. A "fix" that reused fpuClampInput (which is
|
||||
// gated on mode >= 2) would pass the mode-2 tests above but fail this one.
|
||||
TEST(EeRecFpu, MaxSClampsInfOperandAtClampMode1)
|
||||
{
|
||||
FpuClampModeGuard guard(1);
|
||||
EeRecTestHarness h;
|
||||
h.EnableCop1();
|
||||
h.SetFprBits(1, 0x7F800000u); // +Inf
|
||||
h.SetFpr(2, 3.0f);
|
||||
h.LoadProgram({ee::MAX_S(3, 1, 2)});
|
||||
h.RunJitNoDiff();
|
||||
EXPECT_EQ(h.GetFprBitsJit(3), 0x7F7FFFFFu);
|
||||
}
|
||||
|
||||
// Lower bound: at mode 0 x86 skips the operand clamp (fpuFloat2 is a no-op when
|
||||
// !CHECK_FPU_OVERFLOW), so +Inf passes through unclamped. Guards against the fix
|
||||
// over-clamping (making the clamp unconditional). +Inf, not NaN, is used here:
|
||||
// mode-0 MAXSS-vs-Fmaxnm NaN handling is a separate pre-existing divergence.
|
||||
TEST(EeRecFpu, MaxSDoesNotClampAtClampMode0)
|
||||
{
|
||||
FpuClampModeGuard guard(0);
|
||||
EeRecTestHarness h;
|
||||
h.EnableCop1();
|
||||
h.SetFprBits(1, 0x7F800000u); // +Inf
|
||||
h.SetFpr(2, 3.0f);
|
||||
h.LoadProgram({ee::MAX_S(3, 1, 2)});
|
||||
h.RunJitNoDiff();
|
||||
EXPECT_EQ(h.GetFprBitsJit(3), 0x7F800000u); // +Inf, unclamped
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Allocator-state interaction patterns — consecutive ops sharing operands.
|
||||
// The allocator path keeps operands in NEON across opcodes; an aliasing or
|
||||
|
||||
Reference in New Issue
Block a user