Fix: RSQRT.S's zero-divisor sign comes from the dividend

Two sign rules, and they are not the same rule. DIV.S takes the xor of
both operands. RSQRT.S takes the DIVIDEND's sign alone -- it divides by
sqrt(|Ft|), so the divisor has no sign left to contribute by the time the
division happens. Both of our engines took Ft's sign, and the arm64
emitter was alone among recompilers in it: x86 recRSQRThelper1 (iFPU.cpp)
has always taken Fs's.

The console rows that separate the rules: rsqrt(+0, -0) is positive and
rsqrt(-0, -0) is negative on silicon; an xor rule, or Ft's sign, flips
both. Fixing the sign moves the arm64 emitter's agreement with the
console and with the x86 JIT, and keeps the two local engines in exact
agreement on the whole zero path.

The MAGNITUDE stays at the fast tier's +/-fMax saturation. Silicon
returns 0x7FFFFFFF there -- the EE's real maximum, one binade up -- but
that is the top-binade compromise shared by every fast-path op, not the
sign rule, and it moves as a class or not at all.

Pinned by EeRecFpuRsqrt.ZeroDivisorSignComesFromTheDividend, six rows
across both zero-sign combinations and nonzero dividends, both engines
diffed. DenormalDivisorTreatedAsZero's expectation flips to the new rule.

Idea by pstef.
This commit is contained in:
bmdhacks
2026-08-02 22:35:45 -07:00
committed by Brian Degenhardt
parent 665533c738
commit 8f67945c9e
3 changed files with 69 additions and 8 deletions
+9 -1
View File
@@ -343,7 +343,15 @@ void RSQRT_S() {
if ( ( _FtValUl_ & 0x7F800000 ) == 0 ) { // Ft is zero (Denormals are Zero)
_ContVal_ |= FPUflagD | FPUflagSD;
_FdValUl_ = ( _FtValUl_ & 0x80000000 ) | posFmax;
// The sign of FS ALONE. Unlike DIV.S there is no xor here: rsqrt
// divides by sqrt(|Ft|), so the divisor has no sign left to contribute
// by the time the division happens. Console rows witness it --
// rsqrt(+0, -0) is positive and rsqrt(-0, -0) is negative, and an xor
// rule (or Ft's sign, which this used) flips both. x86 recRSQRThelper1
// has always taken Fs's sign. The magnitude stays at posFmax, the
// shared saturation compromise -- silicon says 0x7FFFFFFF there, which
// is the top-binade question, not the sign question.
_FdValUl_ = ( _FsValUl_ & 0x80000000 ) | posFmax;
return;
}
else if ( _FtValUl_ & 0x80000000 ) { // Ft is negative
+17 -4
View File
@@ -1164,13 +1164,26 @@ static void recRSQRT_S_xmm(int info)
armAsm->Tst(RWARG1, 0x7F800000);
armAsm->B(&notZero, a64::ne);
// Zero divisor: set D|SD; result = sign(Ft) | 0x7f7fffff.
// Zero divisor: set D|SD; 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.
armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
armAsm->Orr(RWSCRATCH, RWSCRATCH, FPUflagD | FPUflagSD);
armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]);
armAsm->And(RWARG1, RWARG1, 0x80000000);
armAsm->Orr(RWARG1, RWARG1, 0x7f7fffff);
armAsm->Fmov(armSRegister(EEREC_D), RWARG1);
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(&notZero);
@@ -11,7 +11,7 @@
// - FCR31 flags (I|D|SI|SD): match the interpreter on every input, so they
// are always diffed. I|D are cleared each op; SI|SD are sticky.
// - Zero divisor (Ft exponent field == 0, denormals included): exact
// sign(Ft) | 0x7f7fffff, D|SD raised. Matches interp exactly.
// sign(Fs) | 0x7f7fffff, D|SD raised. Matches interp exactly.
// - Negative nonzero divisor: interp rounds sqrt(|Ft|) into a float temp
// before dividing, so its divide is single-precision and matches native
// bit-for-bit. I|SI raised.
@@ -252,7 +252,8 @@ TEST(EeRecFpuRsqrt, SourceAliasesDivisor)
TEST(EeRecFpuRsqrt, DenormalDivisorTreatedAsZero)
{
// A denormal Ft (exp field 0, mantissa nonzero) is "zero" for RSQRT: result
// is sign(Ft) | 0x7f7fffff with D|SD, exactly like +/-0. Negative denormal.
// is sign(FS) | 0x7f7fffff with D|SD, exactly like +/-0. The divisor's sign
// is irrelevant -- see ZeroDivisorSignComesFromTheDividend below.
EeRecTestHarness h;
h.EnableCop1();
h.SetFcr31(0);
@@ -260,11 +261,50 @@ TEST(EeRecFpuRsqrt, DenormalDivisorTreatedAsZero)
h.SetFprBits(2, 0x807FFFFFu); // largest negative denormal
h.LoadProgram({ee::RSQRT_S(3, 1, 2)});
h.Run();
h.ExpectFpr(3, 0xFF7FFFFFu); // sign(Ft)=neg -> -fMax
h.ExpectFpr(3, 0x7F7FFFFFu); // sign(Fs)=pos -> +fMax
EXPECT_EQ(h.JitSnapshot().fprs.fprc[31] & kStickyMask, kD | kSD);
EXPECT_EQ(h.InterpSnapshot().fprs.fprc[31] & kStickyMask, kD | kSD);
}
// RSQRT.S's zero-divisor result takes the DIVIDEND's sign alone -- not the
// xor DIV.S uses, and not Ft's. The op divides by sqrt(|Ft|), so the divisor
// has no sign left to contribute by the time the division happens. Console
// witnesses: rsqrt(+0, -0) is positive and rsqrt(-0, -0) is negative; an xor
// rule, or Ft's sign (which both engines used to take), flips both. x86
// recRSQRThelper1 (iFPU.cpp) has always taken Fs's sign. The magnitude stays
// at the fast tier's +/-fMax saturation -- silicon says 0x7FFFFFFF, which is
// the top-binade compromise, not the sign rule.
TEST(EeRecFpuRsqrt, ZeroDivisorSignComesFromTheDividend)
{
struct Row
{
u32 fs, ft, want;
};
static const Row kRows[] = {
{0x00000000u, 0x80000000u, 0x7F7FFFFFu}, // rsqrt(+0, -0) -> +fMax
{0x80000000u, 0x00000000u, 0xFF7FFFFFu}, // rsqrt(-0, +0) -> -fMax
{0x80000000u, 0x80000000u, 0xFF7FFFFFu}, // rsqrt(-0, -0) -> -fMax
{0x00000000u, 0x00000000u, 0x7F7FFFFFu}, // rsqrt(+0, +0) -> +fMax
{0x40A00000u, 0x80000000u, 0x7F7FFFFFu}, // rsqrt(+5, -0) -> +fMax
{0xC0A00000u, 0x00000000u, 0xFF7FFFFFu}, // rsqrt(-5, +0) -> -fMax
};
for (const Row& r : kRows)
{
SCOPED_TRACE(::testing::Message() << std::hex << "fs=" << r.fs
<< " ft=" << r.ft);
EeRecTestHarness h;
h.EnableCop1();
h.SetFcr31(0);
h.SetFprBits(1, r.fs);
h.SetFprBits(2, r.ft);
h.LoadProgram({ee::RSQRT_S(3, 1, 2)});
h.Run(); // exact on the zero path: auto-diffs the engines
h.ExpectFpr(3, r.want);
EXPECT_EQ(h.JitSnapshot().fprs.fprc[31] & kStickyMask, kD | kSD);
EXPECT_EQ(h.InterpSnapshot().fprs.fprc[31] & kStickyMask, kD | kSD);
}
}
// ---- FCR31 sticky-bit contract: clear I|D each op, preserve SI|SD -----------
TEST(EeRecFpuRsqrt, ClearsIDPreservesStickyOnPositive)