Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions core/libs/prx/libc/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ target_include_directories(libc PRIVATE

target_link_libraries(libc PRIVATE zarchive)

target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/FloatingPoint.cpp)

if(BUILD_TESTING)
add_test_executable(guest_floating_point_tests ${LIBS_INCLUDE_DIR}/tests/GuestFloatingPoint.cpp)
target_include_directories(guest_floating_point_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_floating_point_tests PRIVATE libc)
configure_windows_unwind(guest_floating_point_tests)
add_test(NAME guest_floating_point COMMAND guest_floating_point_tests)
set_tests_properties(guest_floating_point PROPERTIES TIMEOUT 10)
if(WIN32)
set_tests_properties(guest_floating_point PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()

if(WIN32)
target_link_libraries(libc PRIVATE advapi32 ws2_32 iphlpapi)
endif()
Expand Down
70 changes: 70 additions & 0 deletions core/libs/prx/libc/src/FloatingPoint.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <array>
#include <cstdint>
#include <stdexcept>
#include <xmmintrin.h>

namespace {

constexpr unsigned Exceptions = 0x3f;
constexpr unsigned Rounding = 0xc00;

void ReplaceFlags(unsigned mask, unsigned flags) {
std::array<std::uint32_t, 7> environment;
asm volatile("fnstenv %0" : "=m"(environment));
environment[1] = (environment[1] & ~mask) | (flags & mask);
asm volatile("fldenv %0" : : "m"(environment));
_mm_setcsr((_mm_getcsr() & ~mask) | (flags & mask));
}

}

extern "C" {

int APS5_VABI fegetround_nid_postfix() {
std::uint16_t control;
asm volatile("fnstcw %0" : "=m"(control));
return control & Rounding;
}

int APS5_VABI fesetround_nid_postfix(int rounding) {
const auto mode = static_cast<unsigned>(rounding);
if ((mode & ~Rounding) != 0) return -1;
std::uint16_t control;
asm volatile("fnstcw %0" : "=m"(control));
control = static_cast<std::uint16_t>((control & ~Rounding) | mode);
asm volatile("fldcw %0" : : "m"(control));
_mm_setcsr((_mm_getcsr() & ~(Rounding << 3)) | (mode << 3));
return 0;
}

int APS5_VABI fetestexcept_nid_postfix(int exceptions) {
std::uint16_t status;
asm volatile("fnstsw %0" : "=am"(status));
return (status | _mm_getcsr()) & static_cast<unsigned>(exceptions) & Exceptions;
}

int APS5_VABI fegetexceptflag_nid_postfix(std::uint16_t* flags, int exceptions) {
if (flags == nullptr) throw std::invalid_argument("fegetexceptflag: null flags");
*flags = static_cast<std::uint16_t>(fetestexcept_nid_postfix(exceptions));
return 0;
}

int APS5_VABI fesetexceptflag_nid_postfix(const std::uint16_t* flags, int exceptions) {
if (flags == nullptr) throw std::invalid_argument("fesetexceptflag: null flags");
ReplaceFlags(static_cast<unsigned>(exceptions) & Exceptions, *flags);
return 0;
}

int APS5_VABI feclearexcept_nid_postfix(int exceptions) {
const auto mask = static_cast<unsigned>(exceptions) & Exceptions;
if (mask == Exceptions) {
asm volatile("fnclex");
_mm_setcsr(_mm_getcsr() & ~Exceptions);
} else if (mask != 0) {
ReplaceFlags(mask, 0);
}
return 0;
}

}
165 changes: 165 additions & 0 deletions core/libs/tests/GuestFloatingPoint.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <array>
#include <barrier>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <stdexcept>
#include <thread>
#include <xmmintrin.h>

extern "C" {
int APS5_VABI fegetround_nid_postfix();
int APS5_VABI fesetround_nid_postfix(int);
int APS5_VABI fetestexcept_nid_postfix(int);
int APS5_VABI feclearexcept_nid_postfix(int);
int APS5_VABI fegetexceptflag_nid_postfix(std::uint16_t*, int);
int APS5_VABI fesetexceptflag_nid_postfix(const std::uint16_t*, int);
}

namespace {

struct Environment {
std::array<std::uint32_t, 7> x87;
std::uint32_t sse;
};

Environment ReadEnvironment() {
Environment state;
asm volatile("fnstenv %0\n\tfldenv %0" : "=m"(state.x87));
state.sse = _mm_getcsr();
return state;
}

void WriteEnvironment(const Environment& state) {
asm volatile("fldenv %0" : : "m"(state.x87));
_mm_setcsr(state.sse);
}

void Require(bool value, const char* message) {
if (!value) {
std::fprintf(stderr, "floating-point environment: %s\n", message);
std::abort();
}
}

Environment SeedFlags(unsigned x87, unsigned sse) {
auto state = ReadEnvironment();
state.x87[0] |= 0x3f;
state.x87[1] = (state.x87[1] & ~0xffu) | x87;
state.sse = (state.sse & ~0x3fu) | (0x3fu << 7) | sse;
WriteEnvironment(state);
return ReadEnvironment();
}

void CheckRounding() {
const auto seeded = SeedFlags(0x02, 0x10);
constexpr int modes[] = {0, 0x400, 0x800, 0xc00};
constexpr int positive[] = {2, 1, 2, 1};
constexpr int negative[] = {-2, -2, -1, -1};
for (unsigned index = 0; index < 4; ++index) {
const auto before = ReadEnvironment();
Require(fesetround_nid_postfix(modes[index]) == 0, "set rounding mode");
Require(fegetround_nid_postfix() == modes[index], "query rounding mode");
const auto state = ReadEnvironment();
Require((state.x87[0] & 0xc00) == static_cast<unsigned>(modes[index]), "x87 rounding bits");
Require((state.sse & 0x6000) == static_cast<unsigned>(modes[index] << 3), "SSE rounding bits");
Require((state.x87[0] & ~0xc00u) == (seeded.x87[0] & ~0xc00u), "preserve x87 control bits");
Require((state.sse & ~0x603fu) == (seeded.sse & ~0x603fu), "preserve SSE control bits");
Require(state.x87[1] == before.x87[1] && (state.sse & 0x3f) == (before.sse & 0x3f), "rounding preserves exceptions");
for (const double input : {1.75, -1.75}) {
const int expected = input > 0 ? positive[index] : negative[index];
int x87;
asm volatile("fldl %1\n\tfistpl %0" : "=m"(x87) : "m"(input) : "st");
Require(x87 == expected, "x87 arithmetic observes rounding");
int sse;
asm volatile("cvtsd2si %1, %0" : "=r"(sse) : "x"(input));
Require(sse == expected, "SSE arithmetic observes rounding");
}
}
for (const int invalid : {-1, 1, 0x200, 0x1000, 0x7fffffff}) {
const auto before = ReadEnvironment();
Require(fesetround_nid_postfix(invalid) == -1, "reject invalid rounding mode");
const auto after = ReadEnvironment();
Require(before.x87[0] == after.x87[0] && before.sse == after.sse, "invalid mode leaves state unchanged");
}
auto distinct = ReadEnvironment();
distinct.x87[0] = (distinct.x87[0] & ~0xc00u) | 0x400;
distinct.sse = (distinct.sse & ~0x6000u) | 0x4000;
WriteEnvironment(distinct);
Require(fegetround_nid_postfix() == 0x400, "query uses the x87 rounding mode");
}

void CheckFlags() {
const auto initial = SeedFlags(0x03, 0x24);
Require(fetestexcept_nid_postfix(0) == 0, "empty exception query");
Require(fetestexcept_nid_postfix(0x3f) == 0x27, "combine x87 and SSE flags, including denormals");
Require(fetestexcept_nid_postfix(0x0c) == 0x04, "query only requested flags");
struct { std::uint16_t before, value, after; } saved{0x1234, 0xffff, 0x5678};
Require(fegetexceptflag_nid_postfix(&saved.value, 0x07) == 0 && saved.value == 0x07, "save selected flags");
Require(saved.before == 0x1234 && saved.after == 0x5678, "fexcept_t writes exactly two bytes");
auto state = ReadEnvironment();
Require(state.x87[1] == initial.x87[1] && state.sse == initial.sse, "queries do not change flags");

const std::uint16_t flags = 0x22;
Require(fesetexceptflag_nid_postfix(&flags, 0x0a) == 0, "restore selected flags");
state = ReadEnvironment();
Require((state.x87[1] & 0x3f) == 0x03 && (state.sse & 0x3f) == 0x26, "restore only selected flags on both units");
Require(fesetexceptflag_nid_postfix(&flags, 0) == 0, "empty restore");
Require(feclearexcept_nid_postfix(0x03) == 0, "clear selected flags");
state = ReadEnvironment();
Require((state.x87[1] & 0x3f) == 0 && (state.sse & 0x3f) == 0x24, "clear flags on both units");
Require(state.x87[0] == initial.x87[0], "flag operations preserve x87 control");
Require((state.sse & ~0x3fu) == (initial.sse & ~0x3fu), "flag operations preserve SSE control");
Require(feclearexcept_nid_postfix(0) == 0 && fetestexcept_nid_postfix(0x3f) == 0x24, "empty clear");
SeedFlags(0x3f, 0x3f);
Require(feclearexcept_nid_postfix(0x3f) == 0 && fetestexcept_nid_postfix(0x3f) == 0, "clear every exception");
Require(fegetexceptflag_nid_postfix(&saved.value, 0) == 0 && saved.value == 0, "save empty mask");
auto traps = ReadEnvironment();
traps.x87[0] &= ~0x08u;
traps.sse &= ~(0x08u << 7);
WriteEnvironment(traps);
const std::uint16_t none = 0;
Require(fesetexceptflag_nid_postfix(&none, 0x01) == 0, "restore with an unmasked exception");
Require(feclearexcept_nid_postfix(0x02) == 0, "clear with an unmasked exception");
state = ReadEnvironment();
Require(state.x87[0] == traps.x87[0] && state.sse == traps.sse, "flag updates preserve exception masks");
SeedFlags(0, 0);
for (const bool get : {false, true}) {
bool threw = false;
try {
if (get) fegetexceptflag_nid_postfix(nullptr, 0x3f);
else fesetexceptflag_nid_postfix(nullptr, 0x3f);
} catch (const std::invalid_argument&) { threw = true; }
Require(threw, "reject null exception storage");
}
}

void CheckThreads() {
Require(fesetround_nid_postfix(0) == 0, "reset main rounding mode");
Require(feclearexcept_nid_postfix(0x3f) == 0, "reset main exceptions");
std::barrier ready(2);
const auto worker = [&](int rounding, std::uint16_t flags) {
Require(fesetround_nid_postfix(rounding) == 0, "thread rounding mode");
Require(fesetexceptflag_nid_postfix(&flags, 0x3f) == 0, "thread exceptions");
ready.arrive_and_wait();
Require(fegetround_nid_postfix() == rounding, "thread-local rounding mode");
Require(fetestexcept_nid_postfix(0x3f) == flags, "thread-local exception flags");
};
std::thread first(worker, 0x400, 0x01);
std::thread second(worker, 0x800, 0x04);
first.join();
second.join();
Require(fegetround_nid_postfix() == 0 && fetestexcept_nid_postfix(0x3f) == 0, "worker state does not change main");
}

}

int main() {
const auto original = ReadEnvironment();
CheckRounding();
CheckFlags();
CheckThreads();
WriteEnvironment(original);
std::puts("floating-point environment checks passed");
}
1 change: 1 addition & 0 deletions docs/dev/debt/libc.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- [_Thrd_sleep](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `jfRI3snge3o`) follows the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`xtime.c`) and C11 Annex K, without console verification. It reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep`, retrying on `EINTR`, so it is a cancellation point; a null xtime or one whose normalization overflows throws. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched.
- [_Mtx_init_with_name](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `tgioGpKtmbE`) forwards to `_Mtx_init` and ignores the mutex name parameter.
- [_Cnd_timedwait](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `McaImWKXong`) follows the console's `_Cnd_timedwait` (disassembled from a title's `libc.prx`), where xtime is an absolute deadline converted against the current time: it returns `_Thrd_timedout` (2) on expiration and `_Thrd_success` (0) on notification. The console returns 4 for other internal pthread errors while this implementation throws.
- [fegetround, fesetround, fetestexcept, feclearexcept, fegetexceptflag and fesetexceptflag](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc) follow FreeBSD 11.4 amd64 [fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): rounding modes use x87 control bits 10-11, exception flags include denormals (`0x3f`), and `fexcept_t` is 16 bits. They access the current host thread's x87 and MXCSR state. This console ABI is assumed, as for `fegetenv`, not measured on hardware. Null flag pointers throw instead of faulting.
- [fegetenv](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `psx0YiAKm7k`) follows FreeBSD 11 amd64 [fenv.c](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/amd64/fenv.c) with the 32-byte `fenv_t` of [x86/fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): `fnstenv` fills the 28-byte x87 part, MXCSR follows at 0x1c, and the x87 control word is reloaded because `fnstenv` masks every x87 exception. That the console's `libSceLibcInternal` has the same code and layout is assumed, not read from its binary. A null pointer throws instead of faulting
- [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws
- [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(const char* heapName)` and its return follow the `libc.prx` ASTRO BOT (PPSA21564 01.007.000) bundles: its four calls (0x6801, 0x7F46, 0xE75E, 0xEE02) are in the `malloc_stats` and `sceLibcMspaceMallocStats` paths, each after a `[%s] ...` line printed with the same heap name buffer, and the caller ignores the result and continues. The console's backtrace output is not reproduced: the heap name and the guest return address are written to the host standard error
Expand Down
Loading