From a0eaa92c0a5674ecf023570b15633124f0e4a1f2 Mon Sep 17 00:00:00 2001 From: pstef <3462925+pstef@users.noreply.github.com> Date: Sat, 15 Aug 2026 15:46:48 +0200 Subject: [PATCH] EE/FPU: narrow SQRT's root with a plain Fcvt ToPS2FPU_Full's saturating and flushing arms are for results that leave the PS2's range, and a root does not, so both sat dead behind their compares. The body goes from 54 host instructions to 23, under the 26 the single-precision path spends. --- pcsx2/arm64/iFPUd-arm64.cpp | 12 ++++++-- .../ee_rec_fpu_full_mode_tests.cpp | 29 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/pcsx2/arm64/iFPUd-arm64.cpp b/pcsx2/arm64/iFPUd-arm64.cpp index de97c3418e..51070a92e1 100644 --- a/pcsx2/arm64/iFPUd-arm64.cpp +++ b/pcsx2/arm64/iFPUd-arm64.cpp @@ -1034,8 +1034,8 @@ void recDIV_S_xmm(int info) void recSQRT_S_xmm(int info) { - // Round-to-nearest for the double Fsqrt + the ToPS2FPU narrowing, like - // x86's roundmode_nearest swap (FPUDivFPCR is the nearest-mode FPCR). + // Round-to-nearest for the double Fsqrt and the narrowing Fcvt, like x86's + // roundmode_nearest swap (FPUDivFPCR is the nearest-mode FPCR). const bool swapFpcr = EmuConfig.Cpu.FPUFPCR.bitmask != EmuConfig.Cpu.FPUDivFPCR.bitmask; if (swapFpcr) emitLoadFPCRImm(EmuConfig.Cpu.FPUDivFPCR.bitmask); @@ -1056,7 +1056,13 @@ void recSQRT_S_xmm(int info) armAsm->Bind(&tPositive); armAsm->Fsqrt(armDRegister(treg), armDRegister(treg)); - ToPS2FPU_Full(treg, false, treg, false, false); + // A root cannot leave the in-range band, so the narrowing is the plain + // Fcvt with none of ToPS2FPU_Full's arms around it. The largest operand is + // a shade under 2^129 and roots to under 2^65; the smallest one FZ does + // not flush is 2^-126 and roots to 2^-63. Both sit inside [2^-126, 2^128), + // and the only result outside it is the zero the underflow arm would have + // flushed to the same zero, |t| having already made its sign positive. + armAsm->Fcvt(armSRegister(treg), armDRegister(treg)); SingleToSlot(EEREC_D, treg); _freeNEONreg(treg); diff --git a/tests/ctest/core/recompilers/ee_rec_fpu_full_mode_tests.cpp b/tests/ctest/core/recompilers/ee_rec_fpu_full_mode_tests.cpp index 8a2fdc7593..c1b5135d30 100644 --- a/tests/ctest/core/recompilers/ee_rec_fpu_full_mode_tests.cpp +++ b/tests/ctest/core/recompilers/ee_rec_fpu_full_mode_tests.cpp @@ -588,6 +588,35 @@ TEST(EeRecFpuFull, SqrtNegativeSetsIFlagAndUsesAbs) EXPECT_EQ(h.GetGpr64Jit(reg::v0) & 0x00020040u, 0x00020040u) << "I|SI not set"; } +// recSQRT_S_xmm narrows with a plain Fcvt because a root cannot leave the band +// ToPS2FPU_Full's saturating and flushing arms exist for. These are the four +// operands nearest the ends of that band. +TEST(EeRecFpuFull, SqrtStaysInsideTheNarrowingBand) +{ + struct Case + { + u32 ft, want; + const char* what; + }; + static constexpr Case kCases[] = { + {0x7FFFFFFFu, 0x5FB504F3u, "EEMAX: the largest root there is, 2^64.5"}, + {0x00800000u, 0x20000000u, "2^-126: the smallest operand FZ keeps, root 2^-63"}, + {0x007FFFFFu, 0x00000000u, "the largest denormal, flushed ahead of the root"}, + {0x80000000u, 0x00000000u, "-0.0"}, + }; + for (const Case& c : kCases) + { + SCOPED_TRACE(c.what); + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuFullMode(); + h.SetFprBits(1, c.ft); + h.LoadProgram({SQRT_S(2, 1)}); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetFprBitsJit(2), c.want) << std::hex << "ft=" << c.ft; + } +} + TEST(EeRecFpuFull, RsqrtPseudoInfExact) { // 1.0 / sqrt(2^128) = 2^-64 = 0x1f800000 exactly. The current interp