From 352c27b62c7f6bb0724e9a40bcf39583490bdbc7 Mon Sep 17 00:00:00 2001 From: Rujul Talekar <113801876+Roojool@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:41:00 +0530 Subject: [PATCH] fix(libc): store the gamma sign in an exported signgam --- core/libs/prx/libc/src/Math.cpp | 19 ++++++++++++++++++- core/libs/tests/GuestMath.cpp | 18 ++++++++++++++++++ docs/dev/debt/libc.md | 2 +- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 1a835e2fe87..5ec397f9c41 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -163,10 +163,24 @@ long double AsinDenominator(long double t) { return 1.0L + t * (q[0] + t * (q[1] + t * (q[2] + t * (q[3] + t * q[4])))); } +int GammaSign(double x) { + const std::uint32_t high = static_cast(std::bit_cast(x) >> 32); + const std::uint32_t magnitude = high & 0x7fffffffu; + if (magnitude >= 0x7ff00000u) return 1; + const int direction = 1 - 2 * static_cast(high >> 31); + if (magnitude < 0x3c700000u) return direction; + if (high < 0x80000000u) return 1; + if (magnitude >= 0x43300000u) return 1; + if (std::trunc(x) == x) return 1; + return static_cast(std::ceil(x)) % 2 == 0 ? -1 : 1; +} + } extern "C" { +int signgam_nid_postfix = 0; + std::lldiv_t APS5_VABI lldiv_nid_postfix(long long numerator, long long denominator) { return std::lldiv(numerator, denominator); } @@ -300,7 +314,10 @@ float APS5_VABI log1pf_nid_postfix(float x) { return std::log1p(x); } double APS5_VABI expm1_nid_postfix(double x) { return std::expm1(x); } float APS5_VABI expm1f_nid_postfix(float x) { return std::expm1(x); } float APS5_VABI erff_nid_postfix(float x) { return std::erf(x); } -double APS5_VABI lgamma_nid_postfix(double x) { return std::lgamma(x); } +double APS5_VABI lgamma_nid_postfix(double x) { + signgam_nid_postfix = GammaSign(x); + return std::lgamma(x); +} float APS5_VABI sinf_nid_postfix(float x) { return std::sin(x); } float APS5_VABI cosf_nid_postfix(float x) { return std::cos(x); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index cad91f320d1..8096d01d25f 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -70,6 +70,7 @@ double APS5_VABI expm1_nid_postfix(double); float APS5_VABI expm1f_nid_postfix(float); float APS5_VABI erff_nid_postfix(float); double APS5_VABI lgamma_nid_postfix(double); +extern int signgam_nid_postfix; float APS5_VABI logbf_nid_postfix(float); double APS5_VABI exp2_nid_postfix(double); float APS5_VABI __powisf2_nid_postfix(float, int); @@ -758,6 +759,22 @@ static void CheckNoConversionIsPositiveZero() { Require(parsedWideLong == 0.L && !std::signbit(parsedWideLong) && wideEnd == wide); } +static void CheckLgammaSign() { + Require(std::abs(lgamma_nid_postfix(-0.5) - 1.265512123484645) < 1e-15 && signgam_nid_postfix == -1); + Require(std::abs(lgamma_nid_postfix(-1.5) - 0.8600470153764812) < 1e-15 && signgam_nid_postfix == 1); + Require(std::abs(lgamma_nid_postfix(-2.5) + 0.056243716497674) < 1e-15 && signgam_nid_postfix == -1); + Require(std::abs(lgamma_nid_postfix(1.5) + 0.1207822376352454) < 1e-15 && signgam_nid_postfix == 1); + Require(std::abs(lgamma_nid_postfix(2.5) - 0.2846828704729192) < 1e-15 && signgam_nid_postfix == 1); + Require(std::abs(lgamma_nid_postfix(-1e-100) - 230.25850929940458) < 1e-12 && signgam_nid_postfix == -1); + Require(std::isinf(lgamma_nid_postfix(0.)) && signgam_nid_postfix == 1); + Require(std::isinf(lgamma_nid_postfix(-0.)) && signgam_nid_postfix == -1); + Require(std::isinf(lgamma_nid_postfix(-1.)) && signgam_nid_postfix == 1); + Require(std::isinf(lgamma_nid_postfix(-2.)) && signgam_nid_postfix == 1); + Require(std::isinf(lgamma_nid_postfix(std::numeric_limits::infinity())) && signgam_nid_postfix == 1); + Require(std::isinf(lgamma_nid_postfix(-std::numeric_limits::infinity())) && signgam_nid_postfix == 1); + Require(std::isnan(lgamma_nid_postfix(std::numeric_limits::quiet_NaN())) && signgam_nid_postfix == 1); +} + int main() { CheckFloatClassification(); CheckNoConversionIsPositiveZero(); @@ -837,6 +854,7 @@ int main() { Require(lgamma_nid_postfix(1.) == 0. && lgamma_nid_postfix(2.) == 0. && std::isinf(lgamma_nid_postfix(0.))); Require(std::abs(lgamma_nid_postfix(0.5) - 0.5723649429247001) < 1e-15 && std::abs(lgamma_nid_postfix(10.) - 12.801827480081469) < 1e-13); Require(std::abs(lgamma_nid_postfix(-0.5) - 1.2655121234846454) < 1e-15); + CheckLgammaSign(); Require(llroundl_nid_postfix(2.5L) == 3 && llroundl_nid_postfix(-2.5L) == -3 && llroundl_nid_postfix(0.49999999999999999L) == 0); Require(llroundl_nid_postfix(9007199254740993.0L) == 9007199254740993LL && llroundl_nid_postfix(-1e30L) == INT64_MIN); Require(logbf_nid_postfix(8.f) == 3.f && logbf_nid_postfix(-0.75f) == -1.f); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 124436d2ad1..1c37d68b3fc 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -34,7 +34,7 @@ - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `NAN(n-char-sequence)` converts to the quiet NaN that `nan` and `nanf` return, negated after a minus sign, and ignores the sequence as they ignore their tag. This is inferred from C17 7.12.11.2p2, which makes `nan("n")` equivalent to `strtod("NAN(n)", NULL)`, and not observed on the console. No console `nanl` was observed, so `strtold` returns the x87 default quiet NaN (exponent `0x7fff`, significand `0xc000000000000000`) - [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. -- [lgamma](../../../core/libs/prx/libc/src/Math.cpp) (libc) returns the host's `lgamma` but does not store the sign of Γ(x) in the guest's `signgam`, as FreeBSD 11's [`e_lgamma.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/e_lgamma.c) does through `__ieee754_lgamma_r`, because no guest `signgam` is exported. PPSA99004 imports it. Console behaviour is unverified +- [lgamma](../../../core/libs/prx/libc/src/Math.cpp) (libc) takes |Γ(x)| from the host's `lgamma`, so it can differ from the console's FreeBSD msun code in the last bit, and fills the guest `signgam` that FreeBSD 11's [`e_lgamma.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/e_lgamma.c) passes to [`__ieee754_lgamma_r`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/e_lgamma_r.c) and that file's own rule derives from the argument: +1 at a pole, at the infinities and for NaN, -1 for a negative zero and otherwise, for x < 0, the sign of sin(pi*x), which is negative exactly where ceil(x) is even. `signgam` is a plain global here as it is on FreeBSD, so concurrent `lgamma` calls, and a guest reading it, race the same way. The magnitude on Windows comes from the CRT, which has no `lgamma_r`, so the sign rule is evaluated here instead of being taken from the host. `lgammaf` and `lgammal` are not exported here, so no other call fills `signgam`. The `libc.prx` bundled with PPSA59322 and PPSA76677 exports `signgam` (NID `5gOOC0kzW0c`) as a 4-byte global, so the console does too, but neither of those two titles imports it and no title available here does; console behaviour is unverified - [sinhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed - [asinl](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. The result needs the x87 precision control at 64 bits, as FreeBSD amd64 starts threads (control word `0x37f`), but guest threads do not set it: on Windows a thread started by `_beginthreadex` runs at 53 bits (`0x27f`), where `asinl(0.75)` returns mantissa `0xd91a98ae3406e000` instead of `0xd91a98ae3406e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code - [atanhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `atanhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_atanhf.c`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_atanhf.c) calling msun's [`log1pf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_log1pf.c), whose paths for the positive arguments it passes are inlined here; the exported `log1pf` is still the host's. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, and for |x| = 1 it raises `FE_DIVBYZERO` and sets `errno` to `ERANGE` before returning `x / 0`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits, `errno` and floating-point exceptions as this implementation for all 2^32 arguments; it was not run on Linux. That the system `libSceLibcInternal`, which Dusklight (PPSA99305) imports while bundling a zero-return `libc.prx`, has the same code is assumed