feat(libc): implement asinl

RetroArch 1.0.0-beta.1 (PPSA99169) stops on main 0f863c10 at

    FAIL: unresolved ELF import Nx-F5v0-qU8 from libSceLibcInternal.prx

Nx-F5v0-qU8 is asinl (tools/nid_names.py, aerolib).

The implementation follows the asinl of the libc.prx bundled with
PPSA28319, read from its disassembly: FreeBSD msun's e_asinl.c with
the ld80 invtrig.c coefficients (9f043204), which the binary holds as
the same 80-bit constants, computed in the x87 long double the guest
uses. +-1 returns +-pi/2 rounded to 64 bits and +-0 keeps its sign.
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 sets EDOM too.

20 million random arguments in (-1, 1) plus the branch edges give the
same bits as that code from the title's libc.prx run on a Windows
host. guest_math pins a set of those bits and checks the domain
errors' errno and FE_INVALID, and that errno is untouched otherwise.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
screamm 2026-10-11 14:07:34 +02:00
parent 2c6e3f53ff
commit 5f3fb3dddd
3 changed files with 123 additions and 0 deletions

View File

@ -7,6 +7,7 @@
#include <cstdlib>
#include <cstdint>
#include <cstring>
#include <limits>
#include <stdexcept>
#include "prx/libc/include/General.hpp"
@ -78,6 +79,52 @@ float SinhfExp(float x) {
return y * std::bit_cast<float>(static_cast<std::uint32_t>(0x7f + k) << 23);
}
static_assert(std::numeric_limits<long double>::digits == 64, "guest long double is the x87 80-bit format");
struct ExtendedParts {
std::uint64_t mantissa;
std::uint16_t signExponent;
};
ExtendedParts SplitExtended(long double value) {
unsigned char bytes[sizeof(long double)];
std::memcpy(bytes, &value, sizeof(value));
ExtendedParts parts;
std::memcpy(&parts.mantissa, bytes, sizeof(parts.mantissa));
std::memcpy(&parts.signExponent, bytes + sizeof(parts.mantissa), sizeof(parts.signExponent));
return parts;
}
long double ClearLowMantissaWord(long double value) {
unsigned char bytes[sizeof(long double)];
std::memcpy(bytes, &value, sizeof(value));
std::memset(bytes, 0, sizeof(std::uint32_t));
std::memcpy(&value, bytes, sizeof(value));
return value;
}
constexpr long double AsinNumeratorCoefficients[] = {
1.66666666666666666631e-01L, -4.16313987993683104320e-01L, 3.69068046323246813704e-01L, -1.36213932016738603108e-01L,
1.78324189708471965733e-02L, -2.19216428382605211588e-04L, -7.10526623669075243183e-06L,
};
constexpr long double AsinDenominatorCoefficients[] = {
-2.94788392796209867269e+00L, 3.27309890266528636716e+00L, -1.68285799854822427013e+00L, 3.90699412641738801874e-01L,
-3.14365703596053263322e-02L,
};
constexpr long double HalfPiHigh = 1.57079632679489661926e+00L;
constexpr long double HalfPiLow = -2.50827880633416601173e-20L;
constexpr long double QuarterPiHigh = 7.85398163397448309628e-01L;
long double AsinNumerator(long double t) {
const auto& p = AsinNumeratorCoefficients;
return t * (p[0] + t * (p[1] + t * (p[2] + t * (p[3] + t * (p[4] + t * (p[5] + t * p[6]))))));
}
long double AsinDenominator(long double t) {
const auto& q = AsinDenominatorCoefficients;
return 1.0L + t * (q[0] + t * (q[1] + t * (q[2] + t * (q[3] + t * q[4]))));
}
}
extern "C" {
@ -147,6 +194,36 @@ double APS5_VABI trunc_nid_postfix(double x) { return std::trunc(x); }
double APS5_VABI fma_nid_postfix(double x, double y, double z) { return std::fma(x, y, z); }
long double APS5_VABI acosl_nid_postfix(long double x) { return std::acos(x); }
long double APS5_VABI asinl_nid_postfix(long double x) {
const ExtendedParts parts = SplitExtended(x);
const int exponent = parts.signExponent & 0x7fff;
if (exponent >= 0x3fff) {
if (exponent == 0x3fff && (parts.mantissa & 0x7fffffffffffffffull) == 0) return x * HalfPiHigh + x * HalfPiLow;
std::feraiseexcept(FE_INVALID);
errno = EDOM;
return (x - x) / (x - x);
}
if (exponent < 0x3ffe) {
if (exponent < 0x3fff - 32 && 1.0e300L + x > 1.0L) return x;
const long double t = x * x;
return x + x * (AsinNumerator(t) / AsinDenominator(t));
}
const long double t = (1.0L - std::fabs(x)) * 0.5L;
const long double p = AsinNumerator(t);
const long double q = AsinDenominator(t);
const long double s = std::sqrt(t);
long double result;
if ((parts.mantissa >> 32) >= 0xf3333333u) {
result = HalfPiHigh - (2.0L * (s + s * (p / q)) - HalfPiLow);
} else {
const long double w = ClearLowMantissaWord(s);
const long double c = (t - w * w) / (s + w);
const long double r = p / q;
result = QuarterPiHigh - ((2.0L * s * r - (HalfPiLow - 2.0L * c)) - (QuarterPiHigh - 2.0L * w));
}
return (parts.signExponent & 0x8000) != 0 ? -result : result;
}
long double APS5_VABI frexpl_nid_postfix(long double x, int* exponent) {
if (!std::isfinite(x)) return x;
return std::frexp(x, exponent);

View File

@ -31,6 +31,7 @@ int APS5_VABI abs_nid_postfix(int);
double APS5_VABI trunc_nid_postfix(double);
double APS5_VABI fma_nid_postfix(double, double, double);
long double APS5_VABI acosl_nid_postfix(long double);
long double APS5_VABI asinl_nid_postfix(long double);
long double APS5_VABI frexpl_nid_postfix(long double, int*);
struct LibcFloatConstant { std::uint32_t bits[4]; };
extern LibcFloatConstant _FInf_nid_postfix;
@ -185,6 +186,49 @@ static void CheckFmodErrno() {
Require(fmod_nid_postfix(1e300, 3.0) == std::fmod(1e300, 3.0));
}
static long double MakeExtended(std::uint64_t mantissa, std::uint16_t signExponent) {
unsigned char bytes[sizeof(long double)]{};
std::memcpy(bytes, &mantissa, 8);
std::memcpy(bytes + 8, &signExponent, 2);
long double value = 0;
std::memcpy(&value, bytes, sizeof(value));
return value;
}
static void CheckAsinl() {
const struct { std::uint64_t inputMantissa; std::uint16_t inputSignExponent; std::uint64_t outputMantissa; std::uint16_t outputSignExponent; } cases[] = {
{0, 0x0000, 0, 0x0000}, {0, 0x8000, 0, 0x8000},
{0x8000000000000000ull, 0x3fd7, 0x8000000000000000ull, 0x3fd7}, {0x8000000000000000ull, 0xbfd7, 0x8000000000000000ull, 0xbfd7},
{0x8000000000000000ull, 0x3ffd, 0x815f4e705c3e68f3ull, 0x3ffd}, {0x8000000000000000ull, 0xbffd, 0x815f4e705c3e68f3ull, 0xbffd},
{0xcccccccccccccccdull, 0x3ffb, 0xcd2493b01bc41e02ull, 0x3ffb},
{0x8000000000000000ull, 0x3ffe, 0x860a91c16b9b2c23ull, 0x3ffe}, {0x8000000000000000ull, 0xbffe, 0x860a91c16b9b2c23ull, 0xbffe},
{0xc000000000000000ull, 0xbffe, 0xd91a98ae3406e041ull, 0xbffe}, {0xe666666666666666ull, 0x3ffe, 0x8f549b82fd0b8fa3ull, 0x3fff},
{0xf333333333333333ull, 0x3ffe, 0xa06a08acf9a04f78ull, 0x3fff}, {0xfd70a3d70a3d70a4ull, 0xbffe, 0xb6f1e3799ea8ecc3ull, 0xbfff},
{0xffffffffffffffffull, 0x3ffe, 0xc90fdaa16c63cf01ull, 0x3fff},
{0x8000000000000000ull, 0x3fff, 0xc90fdaa22168c235ull, 0x3fff}, {0x8000000000000000ull, 0xbfff, 0xc90fdaa22168c235ull, 0xbfff},
};
for (const auto& test : cases) {
*__error_nid_postfix() = 12345;
const long double result = asinl_nid_postfix(MakeExtended(test.inputMantissa, test.inputSignExponent));
if (!ExtendedBits(result, test.outputMantissa, test.outputSignExponent) || *__error_nid_postfix() != 12345) {
std::fprintf(stderr, "Guest asinl failed for %016llx:%04x\n", static_cast<unsigned long long>(test.inputMantissa), test.inputSignExponent);
std::abort();
}
}
Require(std::fabs(asinl_nid_postfix(0.5L) - 0.523598775598298873077107230547L) <= 0x1p-64L);
const long double domainErrors[] = {
1.5L, -2.0L, MakeExtended(0x8000000000000001ull, 0x3fff), MakeExtended(0x8000000000000001ull, 0xbfff),
std::numeric_limits<long double>::max(), std::numeric_limits<long double>::infinity(), -std::numeric_limits<long double>::infinity(),
std::numeric_limits<long double>::quiet_NaN(),
};
for (const long double input : domainErrors) {
std::feclearexcept(FE_ALL_EXCEPT);
*__error_nid_postfix() = 0;
Require(std::isnan(asinl_nid_postfix(input)) && *__error_nid_postfix() == EDOM && std::fetestexcept(FE_INVALID) != 0);
}
*__error_nid_postfix() = 0;
}
static void CheckIntegerConversions() {
for (const long long numerator : {4294967301LL, -4294967301LL}) {
for (const long long denominator : {3LL, -3LL}) {
@ -575,6 +619,7 @@ int main() {
CheckSquareRoot();
CheckExactOperations();
CheckLongDouble();
CheckAsinl();
Require(scalbn_nid_postfix(0.75, -2) == 0.1875);
Require(scalbnf_nid_postfix(0.75f, 4) == 12.f);
Require(nextafterf_nid_postfix(1.f, 2.f) == 0x1.000002p0f && nextafterf_nid_postfix(1.f, 0.f) == 0x1.fffffep-1f);

View File

@ -432,6 +432,7 @@ Throughout the project, every function at every stage either **does exactly what
- [Draw input snapshots](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`.
- [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing
- [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
- [SPIRV-Tools optimization](../../core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp) skips CCP and Simplification for modules with storage-only 8- or 16-bit types because the pinned folder produces illegal narrow constants. This also disables unrelated constant folding; the restricted full pipeline mirrors the pinned performance recipe.
- [SRT signed comparison](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) evaluates `SLessThan32` as signed 32-bit less-than, matching the existing SPIR-V backend and [SPIR-V `OpSLessThan`](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpSLessThan). Signed boundary descriptor expressions are tested on the host; native RDNA2/PS5 execution and title behavior remain unverified.
- On macOS the [write watch](../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: