mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
Fix: FULL-mode RSQRT returned -0.0 for the largest magnitude
ToPS2FPU_Full has an arm for values the EE's top binade can hold but a
host single cannot: halve the double, narrow, add 0x00800000 back to the
single. Its guard was |x| >= 2^129, inherited from x86 iFPUd.cpp's
dbl_ps2_overflow. But the largest number this FPU has is 0x7FFFFFFF ==
(2 - 2^-23) * 2^128, a whole binade below 2^129, so everything in the band
(kEeFpuMax, 2^129) was routed into the halving arm when it should have
saturated.
Halved, such a value sits just under 2^128. Under the divide unit's
round-to-NEAREST FPCR the narrow rounds it up to a host infinity and the
+0x00800000 carries out of the exponent field into the sign bit:
0x7f800000 + 0x00800000 == 0x80000000
so the largest magnitude the FPU can produce came back as negative zero --
sign flipped and exponent field 0 rather than 255, which the NFS Carbon
corner (DivZeroOverZeroKeepsPseudoInfExponent) already established is
game-visible through guest softfloat classifiers.
Under the arithmetic FPCR the narrow chops to 0x7f7fffff and the arm is
correct, which is why only the ops that swap to FPUDivFPCR could reach it.
The interpreter's eeRoundToSingle is immune by construction -- it scales by
2^-4, and its comment says why: "the +4 lands on 255 exactly -- it can
never carry into the sign."
ONLY RSQRT REACHES THE BAND, which is why this survived. A DIV quotient
cannot: for 24-bit significands with a < b, a/b <= 1 - 2^-24 strictly, and
the band's relative width is exactly 2^-24. A sweep of the four reachable
exponent differences found 0 hits, and the first probe written for this --
DIV.S(0x7FFFFFFF, 0x3F7FFFFF) -- lands on 2^129 *exactly* and came back
correct, which is what sent me looking for the algebra. SQRT halves
exponents and cannot get near. RSQRT divides by a 53-bit sqrt result, so
the significand argument does not apply; a coarse sweep found 2.5M hits.
The fix is the bound, not the arm: compare against kEeFpuMax's double bit
pattern, with `hi` rather than `hs` because kEeFpuMax itself is
representable and the halving arm handles it exactly (halved it is
+FLT_MAX, and 0x7f7fffff + 0x00800000 == 0x7fffffff). Costs 2 extra
instructions to materialise the constant, on the cold toComplex arm; the
in-range path is untouched.
Verified bidirectionally: the new test's 5 pairs fail on the unpatched
source (jit 80000000, interp 7fffffff on all five) and pass after. The
liveness companion, DivKeepsTopBinadeResultsBelowTheEeMaximum, is green
both ways -- it holds 1.5*2^128 in the halving arm, so over-tightening the
guard down to 2^128 turns it red rather than letting the first test go
green for the wrong reason.
All six ctest binaries green on exit code: recompiler_tests 1640,
core_test 86, mvu_progcache_versioning_tests 13, gs_vertex_tests 21,
common_test 31, demangler_test. The 53 console-conformance and FULL-mode
tests (EeFpuOverflowConsole, EeFpuZeroDivisorConsole, EeRecFpuFull) pass
unchanged, so no capture row moved.
x86 iFPUd.cpp carries the identical constant (s_const DOUBLE(0, 1152, 0)
at :115, consumed at :185) and so has the same defect. Not touched here:
this is an aarch64 host, that column is never executed, and an unverifiable
port is not a fix.
Idea by pstef.
This commit is contained in:
committed by
Brian Degenhardt
parent
25ecdc704d
commit
3dba206e8a
@@ -121,9 +121,24 @@ static void ToPS2FPU_Full(int idx, bool flags, int /*absidx*/, bool acc, bool ad
|
||||
armAsm->B(&end);
|
||||
|
||||
armAsm->Bind(&toComplex);
|
||||
armAsm->Mov(RXARG2, static_cast<u64>(1152) << 52); // dbl_ps2_overflow (2^129)
|
||||
// Saturate above the EE MAXIMUM, not above 2^129.
|
||||
//
|
||||
// x86 iFPUd.cpp uses dbl_ps2_overflow == 2^129 here, but the largest number
|
||||
// this FPU has is 0x7FFFFFFF == (2 - 2^-23) * 2^128, a whole binade below
|
||||
// it. Everything in (that max, 2^129) therefore fell into the halving arm
|
||||
// below, and under the divide unit's round-to-NEAREST FPCR that arm's
|
||||
// +0x00800000 carried out of the exponent field into the SIGN BIT
|
||||
// (0x7f800000 + 0x00800000 == 0x80000000): the largest magnitude the FPU can
|
||||
// produce came back as negative zero. Only RSQRT can land in the band; see
|
||||
// EeRecFpuFull.RsqrtAboveEeMaxSaturatesInsteadOfWrappingToNegativeZero for
|
||||
// why DIV and SQRT cannot.
|
||||
//
|
||||
// `hi`, not `hs`: the EE maximum ITSELF is representable and belongs to the
|
||||
// halving arm, which handles it exactly (halved it is +FLT_MAX, and
|
||||
// 0x7f7fffff + 0x00800000 == 0x7fffffff).
|
||||
armAsm->Mov(RXARG2, UINT64_C(0x47FFFFFFE0000000)); // (2 - 2^-23) * 2^128
|
||||
armAsm->Cmp(RXARG1, RXARG2);
|
||||
armAsm->B(&toOverflow, a64::hs);
|
||||
armAsm->B(&toOverflow, a64::hi);
|
||||
|
||||
// Large but PS2-representable (exp-0xff range): lower double exp, narrow,
|
||||
// raise single exp — the inverse of ToDouble's complex path.
|
||||
|
||||
@@ -612,6 +612,92 @@ TEST(EeRecFpuFull, RsqrtDivByZeroSignedMaxFromDividend)
|
||||
EXPECT_EQ(h.GetGpr64Jit(reg::v0) & 0x00010020u, 0x00010020u) << "D|SD not set";
|
||||
}
|
||||
|
||||
// ToPS2FPU_Full's "large but PS2-representable" arm must not be entered by a
|
||||
// value ABOVE the EE maximum.
|
||||
//
|
||||
// That arm (iFPUd-arm64.cpp) halves the double, narrows, and adds 0x00800000
|
||||
// back to the single. Its guard was |x| >= 2^129 — but the largest number this
|
||||
// FPU has is 0x7FFFFFFF == (2 - 2^-23) * 2^128, which is BELOW 2^129, so the
|
||||
// band (EE max, 2^129) was routed into the halving arm instead of
|
||||
// saturating. Halved, such a value sits just under 2^128; under the divide
|
||||
// unit's round-to-NEAREST FPCR the narrow rounds it up to a host infinity
|
||||
// (0x7f800000) and the +0x00800000 carries out of the exponent field into the
|
||||
// SIGN BIT:
|
||||
//
|
||||
// 0x7f800000 + 0x00800000 == 0x80000000
|
||||
//
|
||||
// so the largest magnitude the FPU can produce came back as negative zero.
|
||||
// Under the arithmetic FPCR (ChopZero) the narrow chops to 0x7f7fffff instead
|
||||
// and the arm is correct, which is why only the ops that swap to FPUDivFPCR
|
||||
// could see it.
|
||||
//
|
||||
// The interpreter cannot wrap this way: it narrows through the host FPU and
|
||||
// saturates at ±FLT_MAX (checkOverflow, FPU.cpp), so it never adds into the
|
||||
// exponent field at all. It also stops a binade below the console's
|
||||
// 0x7FFFFFFF there — a separate, known gap in the interpreter, not this bug.
|
||||
//
|
||||
// ONLY RSQRT REACHES THE BAND. A DIV quotient cannot: for 24-bit significands
|
||||
// with a < b, a/b <= 1 - 2^-24 strictly, and the band's relative width is
|
||||
// exactly 2^-24 (a sweep of the four reachable exponent differences found no
|
||||
// hits, and DIV.S(0x7FFFFFFF, 0x3F7FFFFF) lands on 2^129 *exactly*, which the
|
||||
// >= arm already handled). SQRT halves exponents and cannot get near. RSQRT
|
||||
// divides by a 53-bit sqrt result, so the argument does not apply.
|
||||
//
|
||||
// The operand pairs below were found by solving fs / sqrt(ft) for the band.
|
||||
// The console saturates at 0x7FFFFFFF, and FULL mode now does the same. The
|
||||
// interpreter column is pinned too, at its own saturation bound of
|
||||
// ±FLT_MAX (0x7F7FFFFF) — a binade low against silicon, but positive and
|
||||
// stable: the point here is that neither engine wraps to negative zero.
|
||||
TEST(EeRecFpuFull, RsqrtAboveEeMaxSaturatesInsteadOfWrappingToNegativeZero)
|
||||
{
|
||||
static const u32 kPairs[][2] = {
|
||||
{0x608073EEu, 0x0080E845u}, {0x60814231u, 0x0082878Du},
|
||||
{0x6081A669u, 0x00835244u}, {0x6081B3B0u, 0x00836D2Bu},
|
||||
{0x6081F74Du, 0x0083F655u},
|
||||
};
|
||||
for (const auto& p : kPairs)
|
||||
{
|
||||
EeRecTestHarness h;
|
||||
h.EnableCop1();
|
||||
h.EnableFpuFullMode();
|
||||
h.SetFprBits(0, p[0]);
|
||||
h.SetFprBits(1, p[1]);
|
||||
h.LoadProgram({RSQRT_S(2, 0, 1)});
|
||||
h.RunJitNoDiff();
|
||||
|
||||
// RunJitNoDiff does not run the interpreter, and GetFprBitsInterp would
|
||||
// then hand back the JIT's own value — the reference needs its own run.
|
||||
EeRecTestHarness i;
|
||||
i.EnableCop1();
|
||||
i.SetFprBits(0, p[0]);
|
||||
i.SetFprBits(1, p[1]);
|
||||
i.LoadProgram({RSQRT_S(2, 0, 1)});
|
||||
i.RunInterpOnly();
|
||||
|
||||
EXPECT_EQ(h.GetFprBitsJit(2), 0x7FFFFFFFu)
|
||||
<< "fs=" << p[0] << " ft=" << p[1] << " wrapped";
|
||||
EXPECT_EQ(i.GetFprBitsInterp(2), 0x7F7FFFFFu)
|
||||
<< "interpreter reference moved";
|
||||
}
|
||||
}
|
||||
|
||||
// Liveness for the test above: the halving arm must still be REACHABLE and
|
||||
// exact for the top binade proper. 1.5*2^128 / 1.0 is in the arm's range and
|
||||
// below the EE maximum, so it must come back unrounded. Tightening the overflow
|
||||
// guard too far (down to 2^128) would saturate this to 0x7FFFFFFF and turn the
|
||||
// test above green for the wrong reason.
|
||||
TEST(EeRecFpuFull, DivKeepsTopBinadeResultsBelowTheEeMaximum)
|
||||
{
|
||||
EeRecTestHarness h;
|
||||
h.EnableCop1();
|
||||
h.EnableFpuFullMode();
|
||||
h.SetFprBits(0, 0x7FC00000u); // 1.5 * 2^128
|
||||
h.SetFprBits(1, FloatBits(1.0f));
|
||||
h.LoadProgram({DIV_S(2, 0, 1)});
|
||||
h.RunJitNoDiff();
|
||||
EXPECT_EQ(h.GetFprBitsJit(2), 0x7FC00000u);
|
||||
}
|
||||
|
||||
// GE-M2 residency coherence: FPU-full (DOUBLE-mode) ops hand-emit integer scratch
|
||||
// for the guard-bit alignment (FPU_ADD_SUB) and the min/max bit-pattern build
|
||||
// (recMINMAX). Those temps were RWARG3/RWARG4 (w2/w3) — EE-allocatable pool
|
||||
|
||||
Reference in New Issue
Block a user