Skip to content

feat(libc): keep the guest's x87 long double exactly in lifted builds - #12

Open
oberonlift wants to merge 2 commits into
lifted-libc-va-listfrom
lifted-libc-long-double
Open

oberonlift wants to merge 2 commits into
lifted-libc-va-listfrom
lifted-libc-long-double

Conversation

@oberonlift

@oberonlift oberonlift commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

Open on AnyPS5 as boykopovar#15802; this copy follows it.

ps5lift is a static recompiler for PS5 executables: it lifts a game's x86-64 code to LLVM IR and compiles it ahead of time to aarch64, which runs on Linux against AnyPS5's own libraries with no interpreter or JIT. It runs AnyPS5's PS5-style test programs and a homebrew media player to its home screen; commercial games are not playable yet. Branch ps5lift-preview merges all of its PRs (how to try it); native Apple Silicon support is coming along fast. It is a separate approach from the ARM64 compiler in boykopovar#5021; boykopovar#1835 has an introduction.

This is 6 of 8 PRs that let AnyPS5's libraries run a lifted program; the lifter itself follows them.

x86-64: behaviour unchanged. There GuestLongDouble is long double and its helpers are the standard functions, so strtold is still std::strtold; ldexpl and scalbnl now call std::scalbln and drem calls std::remquo, which return the same values.

What

A lifted libc held the guest's x87 long double in the host's: IEEE binary128 on aarch64 Linux, which holds every 80-bit value but rounds each step differently, and a plain double on macOS, which holds neither.

  • GuestLongDouble loads and stores the guest's 80-bit values where libc takes them in and hands them out (formatting, scanning, strtold, modfl, _LDtest), and long doubles that host callers pass through a va_list are rounded to the guest's precision. On an x87 host it only copies.
  • Lifted builds keep the value in GuestLongDouble, laid out as the guest stores it. Arithmetic, square root, integer rounding and conversions to double, float and integers run through the ps5lift runtime's x87, so they round, honour precision control and raise exceptions as the guest's control and status words say. Comparisons, conversions from integers and doubles, frexp, scalbn, fmod, remainder, remquo, modf, logb and the fixed-direction roundings work on the bits; a NaN operand or a domain error in fmod, remainder and remquo goes through the x87, so the NaN propagates or invalid is raised as on the guest.
  • The _LDint, _LDscale, _LDunscale, _LFpcomp and _LPoly helpers take GuestLongDouble too; in lifted builds _LDscale and _LDunscale scale and split the value on its bits.
  • strtold and scanf("%Lf") parse with exact big-integer arithmetic, correctly rounded in the guest's rounding mode.
  • Builds that are not lifted keep the host's long double wherever it is wider than 80 bits too, such as Emscripten's wasm64 target: there strtold is still the host's.
  • cbrtl runs FreeBSD's x87 algorithm, as on main, on GuestLongDouble, and in lifted builds remainder, remainderf, remquo and remquof take their result and quotient bits from its exact remainder instead of fprem1.
  • The other transcendental l functions and fmal still compute in the host's widest type and round the result. The libc debt notes say so, and record that lifted builds don't build __mulxc3 and __divxc3 and that long double scans store the guest's format for host callers too.

boykopovar#5021 also changes some of the same files: Strings.cpp, WindowsFormatting.hpp.

Tested

Compiled for x86-64 Linux before and after this PR (GCC 13, same flags): 130 of the 139 objects it reaches are byte-identical. libSceNet's Export.cpp differs only in the generated name of a switch table and Strings.cpp in an out-of-line copy of the inline strtold wrapper; the other seven are libc's long double, formatting and scanning sources.

On a DGX Spark (aarch64 Linux, NVIDIA GB10, LLVM 18), with all eight PRs and ps5lift on top: every library builds for a lifted program, 2,288 of AnyPS5's own tests pass in that build on lavapipe, and ps5lift's 69 tests pass, among them its libc, exceptions, kernel, fiber and module-loading fixtures and upstream's libc test programs.

The conventions check passes on this branch.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: feat(libc): read guest va_lists portably in lifted builds #11
  • Fixes: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes

🤖 Generated with Claude Code

@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch from 68aea15 to 2c7c036 Compare November 1, 2026 19:35
@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch from 2c7c036 to b7eea78 Compare November 1, 2026 22:07
@oberonlift
oberonlift force-pushed the lifted-libc-va-list branch 2 times, most recently from 18527d4 to c8cd394 Compare November 2, 2026 17:02
@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch from b7eea78 to 2c34c7a Compare November 2, 2026 17:02
@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch 3 times, most recently from 84e7323 to e405a49 Compare November 3, 2026 16:34
@oberonlift

Copy link
Copy Markdown
Owner Author

Review findings (unverified)

Code review of e405a4983b. None of these have been built, run or checked by a second reviewer yet. Most severe first.

  1. core/libs/prx/libc/include/GuestLongDouble.hpp:509: non-lifted builds with a 113-bit long double truncate in strtold. In such a build (for example a non-lifted aarch64 build), GuestLongDoubleParse rounds to odd and then calls GuestLongDoubleRound. That goes through GuestLongDoubleStore, which drops the low 49 bits instead of rounding, so strtold("0.1") is rounded toward zero. Rounding to odd only works if a round-to-nearest follows. FetchHost's va_list rounding and the 113-bit UnscaledHexadecimal branch use the same function.
  2. core/libs/prx/libc/src/Formatting.cpp:472: non-lifted scanf("%Lf") lost its conversion. The base branch scanned into a host long double and then called GuestLongDoubleStore. Now host sscanf writes straight into the guest's 80-bit slot, so a binary128 host stores IEEE quad bytes where the guest expects x87 bytes. Only the lifted path needed replacing.
  3. core/libs/prx/libc/src/exception/StandardExceptions.cpp:269: an unrelated, ungated change. The vtable call cast condition becomes defined(__APPLE__) || defined(__clang__). That has nothing to do with long double and is not behind ANYPS5_GUEST_LIFTED, so it changes non-Apple clang x86 builds. That contradicts the x86-64: line and the PR's one-topic box.
  4. core/libs/prx/libc/src/DinkumwareInternals.cpp:84: ScaleHexadecimal rounds subnormals as if the value were positive. Upward always rounds the magnitude up, and downward never does. Overflow always returns infinity, and the sign comes later through CopySign. With FE_DOWNWARD, strtold("-0x1.0000000000000001p-16400") truncates where it should round the magnitude up. With FE_TOWARDZERO, "0x1p20000" gives inf instead of LDBL_MAX. The lifted _Stold/_Stoldx hex path (pten == 0) still uses this code.
  5. core/libs/prx/libc/src/MathExact.cpp:362: scalblnl narrows its exponent to int. LibcDetail::Scalbn takes a long, but the exponent is cast to int first. scalblnl(1.0L, 0x100000000) returns 1.0 instead of inf, and 0x100000001 gives 2.0. Clamp it as _LDscale does (±100000), because current + exponent in Scalbn would overflow near INT64_MAX.
  6. core/libs/prx/libc/include/GuestLongDouble.hpp:345: lifted Remainder returns a fresh +nan for NaN inputs and domain errors. It drops the NaN's sign and payload and raises no FE_INVALID. remainderl(INFINITY, 1) gives +nan with the x87 status untouched, where the console gives the negative default NaN and sets IE, and remainderl(-nan, 1) does not pass its input through.
  7. core/libs/prx/libc/include/GuestLongDouble.hpp:78: explicit operator TInteger also matches bool. if (x), !x and x && … then truncate through the x87 int64 store. With x = 0.25, if (x) is false. For NaN or values of 2^63 and above it also raises invalid. Add an explicit operator bool that tests against zero, or exclude bool from the template.
  8. core/libs/prx/libc/include/GuestLongDouble.hpp:83: the conversion to uint64 at or above 2^63 follows the guest's precision control. It subtracts 2^63 with the runtime's x87 subtract. With PC set to 53 bits, (uint64_t)(2^64-1) rounds the shifted value up to 2^63, so ToInt64 gives the indefinite value and the result wraps. NaN and values of 2^64 and above are never range-checked.
  9. core/libs/prx/libc/src/GuestLongDoubleParse.cpp:170: duplicate helpers. It defines its own DigitValue and StartsWithWord, though ScanFloatField.hpp, added in the same PR, already provides LibcDetail::DigitValue, Lower and StartsWord.
  10. core/libs/prx/libc/src/MathExact.cpp:329: lroundl and llroundl take different paths. They are the same function on LP64. lroundl uses LibcDetail::LlRound with no range check, and llroundl (Math.cpp:619) uses RoundToInt64, which range-checks and raises FE_INVALID, so 1e30L gives different exception reporting.

@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch 2 times, most recently from 3e77693 to c9c7bc4 Compare November 4, 2026 16:20
@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch from c9c7bc4 to 093bf45 Compare November 4, 2026 17:17
@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch 2 times, most recently from 524b8de to 3bf0e50 Compare November 5, 2026 01:21
@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch from 3bf0e50 to a68d0af Compare November 5, 2026 15:34
@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch from a68d0af to 3bee83c Compare November 5, 2026 19:28
@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch 2 times, most recently from ccbc82d to e3c107e Compare November 6, 2026 15:35
oberonlift and others added 2 commits November 6, 2026 15:54
Add GuestLongDouble to load and store the guest's x87 80-bit values,
so the libc can compute with a host long double that is not x87
extended, such as aarch64's binary128, when built for a lifted guest.
Formatting, scanning, strtold, modfl, _LDtest and the code that takes
80-bit values apart go through it; strtold parses with round-to-odd
so that the final rounding is correct, and long double arguments that
host callers pass through a va_list are rounded to the guest's precision.
On an x87 host it only copies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A lifted libc held the guest's x87 long double in the host's long double:
IEEE binary128 on Linux, which holds every 80-bit value but rounds each
step differently, and a plain double on macOS, which holds neither.

Lifted builds now keep it in GuestLongDouble, laid out as the guest stores
it. Its arithmetic, square root, integer rounding and conversions to
double, float and integers run through the ps5lift runtime's x87, so they
round, honour precision control and raise exceptions as the guest's
control and status words say. Comparisons, conversions from integers and
doubles, frexp, scalbn, fmod, remainder, remquo, modf, logb and the
fixed-direction roundings work on the bits. strtold and scanf("%Lf") parse
with exact big-integer arithmetic, correctly rounded in the guest's
rounding mode; the narrow scanner reads the field itself, with the wide
scanner's FloatFit, now shared. printf already formatted the exact digits.

On an x86-64 host with x87 behind the runtime's entry points, the parser
matched glibc's x87 strtold in 58,244 cases across the four rounding
modes, and the bit operations matched x87 libm in 5.6 million.

The transcendental l functions and fmal still compute in the host's
widest type and round the result, which on macOS keeps 53 bits; the debt
entry says so. long double complex results are left out of lifted builds,
where they were never bridged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch from e3c107e to e1e808c Compare November 6, 2026 17:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant