diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index fb835934f..e26201927 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -218,16 +218,8 @@ static int RecordWriteAddressFromCounterPair(Apr::CommandBufferObject* buffer, v return AppendCommand(buffer, Apr::Opcode::WriteAddressFromCounterPair, Apr::WriteAddressFromCounterCommand{{}, reinterpret_cast(address), counter, counter + 1u}); } -struct Xtime { - std::int64_t sec; - std::int64_t nsec; -}; - static constexpr int ThrdSuccess = 0; static constexpr int ThrdError = 4; -static constexpr int GuestClockRealtime = 0; -static constexpr std::int64_t NanosPerSecond = 1000000000; -static constexpr std::int64_t LongestSleepSeconds = 86400; static std::atomic_flag sharedPtrSpinLock; @@ -238,22 +230,6 @@ int APS5_VABI _Cnd_init_nid_postfix(void** handle); int SignalCondition_nid_no_patch(void** handle); } -static KernelTimespec XtimeDeadline(const Xtime& time) { - constexpr auto largest = std::numeric_limits::max(); - constexpr auto smallest = std::numeric_limits::min(); - const auto carry = time.nsec / NanosPerSecond; - auto nanos = time.nsec % NanosPerSecond; - auto seconds = time.sec; - if ((carry > 0 && seconds > largest - carry) || (carry < 0 && seconds < smallest - carry)) throw std::overflow_error("_Thrd_sleep: xtime seconds overflow"); - seconds += carry; - if (nanos < 0) { - if (seconds == smallest) throw std::overflow_error("_Thrd_sleep: xtime seconds overflow"); - nanos += NanosPerSecond; - --seconds; - } - return {seconds, nanos}; -} - extern "C" { int APS5_VABI sceAmprCommandBufferWriteAddressOnCompletion(Apr::CommandBufferObject* buffer, volatile std::uint64_t* address, std::uint64_t value) { @@ -943,22 +919,6 @@ void APS5_VABI _Lock_shared_ptr_spin_lock_nid_postfix(void) { while (sharedPtrSpinLock.test_and_set(std::memory_order_acquire)) std::this_thread::yield(); } -int APS5_VABI _Thrd_sleep_nid_postfix(const Xtime* target, Xtime*) { - if (!target) APS5_INVALID_ARG_EX; - const auto deadline = XtimeDeadline(*target); - for (;;) { - KernelTimespec now{}; - if (clock_gettime_nid_postfix(GuestClockRealtime, &now) != 0) throw std::runtime_error("_Thrd_sleep: reading the realtime clock failed"); - if (now.tv_sec > deadline.tv_sec || (now.tv_sec == deadline.tv_sec && now.tv_nsec >= deadline.tv_nsec)) return 0; - KernelTimespec remaining{deadline.tv_sec - now.tv_sec, deadline.tv_nsec - now.tv_nsec}; - if (remaining.tv_nsec < 0) { - remaining.tv_nsec += NanosPerSecond; - --remaining.tv_sec; - } - remaining.tv_sec = std::min(remaining.tv_sec, LongestSleepSeconds); - if (nanosleep_nid_postfix(&remaining, nullptr) != 0) throw std::runtime_error("_Thrd_sleep: nanosleep failed"); - } -} int APS5_VABI wcstombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit) { std::array state{}; diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index a0b57300c..70c2d5261 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -190,6 +190,16 @@ if(BUILD_TESTING) set_tests_properties(guest_libc_threads PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_libc_static_imports_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibcStaticImports.cpp) + target_include_directories(guest_libc_static_imports_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libc_static_imports_tests PRIVATE libc) + configure_windows_unwind(guest_libc_static_imports_tests) + add_test(NAME guest_libc_static_imports COMMAND guest_libc_static_imports_tests) + set_tests_properties(guest_libc_static_imports PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_libc_static_imports PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_fiopen_tests ${LIBS_INCLUDE_DIR}/tests/GuestFiopen.cpp) target_include_directories(guest_fiopen_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_fiopen_tests PRIVATE libc) diff --git a/core/libs/prx/libc/Export.cpp b/core/libs/prx/libc/Export.cpp index be68a6cab..cf23ee77b 100644 --- a/core/libs/prx/libc/Export.cpp +++ b/core/libs/prx/libc/Export.cpp @@ -32,4 +32,8 @@ std::uint64_t APS5_VABI libcCyberUnknown08(void) { return 0; } +APS5_EXPORT("H2e8t5ScQGc", libc___cxa_finalize); +void APS5_VABI libc___cxa_finalize(void* dsoHandle) { + CxaFinalize_nid_no_patch(dsoHandle); +} } diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 23a813338..9ee4532df 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -68,6 +68,7 @@ int APS5_VABI __isfinitef_nid_postfix(float x) { return std::isfinite(x) ? 1 : 0 int APS5_VABI __isnormal_nid_postfix(double x) { return std::isnormal(x) ? 1 : 0; } int APS5_VABI __isnormalf_nid_postfix(float x) { return std::isnormal(x) ? 1 : 0; } int APS5_VABI __isinff_nid_postfix(float x) { return std::isinf(x) ? 1 : 0; } +float APS5_VABI nearbyintf_nid_postfix(float x) { return std::nearbyintf(x); } double APS5_VABI cbrt_nid_postfix(double x) { return std::cbrt(x); } double APS5_VABI asin_nid_postfix(double x) { diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index 4565d8331..44db68abe 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -1,5 +1,8 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/General.hpp" +#include #include +#include #include #include #include @@ -9,9 +12,36 @@ #include #include #include +#include namespace { +struct Xtime { + std::int64_t sec; + std::int64_t nsec; +}; + +constexpr std::int64_t nanosPerSecond = 1000000000; +constexpr std::int64_t longestSleepSeconds = 86400; + +timespec XtimeDeadline(const Xtime& time) { + constexpr auto largest = std::numeric_limits::max(); + constexpr auto smallest = std::numeric_limits::min(); + const auto carry = time.nsec / nanosPerSecond; + auto nanos = time.nsec % nanosPerSecond; + auto seconds = time.sec; + if ((carry > 0 && seconds > largest - carry) || (carry < 0 && seconds < smallest - carry)) + throw std::overflow_error("_Thrd_sleep: xtime seconds overflow"); + seconds += carry; + if (nanos < 0) { + if (seconds == smallest) + throw std::overflow_error("_Thrd_sleep: xtime seconds overflow"); + nanos += nanosPerSecond; + --seconds; + } + return {static_cast(seconds), static_cast(nanos)}; +} + constexpr int threadSuccess = 0; constexpr int threadNomem = 1; constexpr int threadBusy = 3; @@ -111,6 +141,24 @@ int APS5_VABI _Mtx_lock_nid_postfix(void** handle) { return threadSuccess; } +int APS5_VABI _Mtx_trylock_nid_postfix(void** handle) { + auto* mutex = ResolveMutex(handle, "_Mtx_trylock"); + const auto self = std::this_thread::get_id(); + if (mutex->owner.load(std::memory_order_acquire) == self) { + if ((mutex->type & mutexRecursive) == 0) + return threadBusy; + if (mutex->count == std::numeric_limits::max()) + throw std::overflow_error("_Mtx_trylock: recursive lock count overflow"); + ++mutex->count; + return threadSuccess; + } + if (!mutex->native.try_lock()) + return threadBusy; + mutex->owner.store(self, std::memory_order_release); + mutex->count = 1; + return threadSuccess; +} + int APS5_VABI _Mtx_unlock_nid_postfix(void** handle) { auto* mutex = ResolveMutex(handle, "_Mtx_unlock"); if (!IsOwnedByCaller(mutex)) @@ -178,4 +226,28 @@ int SignalCondition_nid_no_patch(void** handle) { return threadSuccess; } +int APS5_VABI _Thrd_sleep_nid_postfix(const Xtime* target, Xtime*) { + if (!target) + APS5_INVALID_ARG_EX; + const auto deadline = XtimeDeadline(*target); + for (;;) { + timespec now{}; + if (clock_gettime(CLOCK_REALTIME, &now) != 0) + throw std::runtime_error("_Thrd_sleep: reading the realtime clock failed"); + if (now.tv_sec > deadline.tv_sec || (now.tv_sec == deadline.tv_sec && now.tv_nsec >= deadline.tv_nsec)) + return threadSuccess; + timespec remaining{static_cast(deadline.tv_sec - now.tv_sec), static_cast(deadline.tv_nsec - now.tv_nsec)}; + if (remaining.tv_nsec < 0) { + remaining.tv_nsec += nanosPerSecond; + --remaining.tv_sec; + } + remaining.tv_sec = std::min(remaining.tv_sec, longestSleepSeconds); + if (nanosleep(&remaining, nullptr) != 0) { + if (errno == EINTR) + continue; + throw std::runtime_error("_Thrd_sleep: nanosleep failed"); + } + } +} + } diff --git a/core/libs/tests/GuestLibcStaticImports.cpp b/core/libs/tests/GuestLibcStaticImports.cpp new file mode 100644 index 000000000..a8068575d --- /dev/null +++ b/core/libs/tests/GuestLibcStaticImports.cpp @@ -0,0 +1,169 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +struct Xtime { + std::int64_t sec; + std::int64_t nsec; +}; + +extern "C" { +float APS5_VABI nearbyintf_nid_postfix(float x); +int APS5_VABI _Mtx_init_nid_postfix(void** handle, int type); +void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); +int APS5_VABI _Mtx_lock_nid_postfix(void** handle); +int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); +int APS5_VABI _Mtx_trylock_nid_postfix(void** handle); +int APS5_VABI _Thrd_sleep_nid_postfix(const Xtime* target, Xtime* remaining); +} + +namespace { + +void Require(bool value) { + if (!value) + std::abort(); +} + +template +void RequireThrows(TCall call) { + try { + call(); + } catch (const TException&) { + return; + } + std::abort(); +} + +constexpr int threadSuccess = 0; +constexpr int threadBusy = 3; +constexpr int mutexTryable = 0x02; +constexpr int mutexRecursive = 0x100; +constexpr std::int64_t nanosPerSecond = 1000000000LL; + +void TestNearbyintf() { + const float half = nearbyintf_nid_postfix(0.5f); + Require(half == 0.0f); + Require(!std::signbit(half)); + + const float negHalf = nearbyintf_nid_postfix(-0.5f); + Require(negHalf == 0.0f); + Require(std::signbit(negHalf)); + + Require(nearbyintf_nid_postfix(1.5f) == 2.0f); + Require(nearbyintf_nid_postfix(-1.5f) == -2.0f); + + Require(nearbyintf_nid_postfix(2.5f) == 2.0f); + Require(nearbyintf_nid_postfix(-2.5f) == -2.0f); + + const float negZero = nearbyintf_nid_postfix(-0.0f); + Require(negZero == 0.0f); + Require(std::signbit(negZero)); + + const float inf = std::numeric_limits::infinity(); + const float posInf = nearbyintf_nid_postfix(inf); + Require(std::isinf(posInf) && posInf > 0.0f); + + const float negInf = nearbyintf_nid_postfix(-inf); + Require(std::isinf(negInf) && negInf < 0.0f); + + const float nan = std::numeric_limits::quiet_NaN(); + Require(std::isnan(nearbyintf_nid_postfix(nan))); +} + +void TestMtxTrylock() { + void* mutex = nullptr; + Require(_Mtx_init_nid_postfix(&mutex, mutexTryable) == threadSuccess && mutex != nullptr); + + Require(_Mtx_trylock_nid_postfix(&mutex) == threadSuccess); + Require(_Mtx_trylock_nid_postfix(&mutex) == threadBusy); + Require(_Mtx_unlock_nid_postfix(&mutex) == threadSuccess); + + std::atomic held{false}; + std::atomic release{false}; + std::thread other([&] { + Require(_Mtx_lock_nid_postfix(&mutex) == threadSuccess); + held.store(true, std::memory_order_release); + while (!release.load(std::memory_order_acquire)) + std::this_thread::yield(); + Require(_Mtx_unlock_nid_postfix(&mutex) == threadSuccess); + }); + + while (!held.load(std::memory_order_acquire)) + std::this_thread::yield(); + + Require(_Mtx_trylock_nid_postfix(&mutex) == threadBusy); + + release.store(true, std::memory_order_release); + other.join(); + + Require(_Mtx_trylock_nid_postfix(&mutex) == threadSuccess); + Require(_Mtx_unlock_nid_postfix(&mutex) == threadSuccess); + _Mtx_destroy_nid_postfix(&mutex); + + void* recMutex = nullptr; + Require(_Mtx_init_nid_postfix(&recMutex, mutexTryable | mutexRecursive) == threadSuccess && recMutex != nullptr); + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadSuccess); + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadSuccess); + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadSuccess); + + std::thread([&] { + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadBusy); + }).join(); + + Require(_Mtx_unlock_nid_postfix(&recMutex) == threadSuccess); + std::thread([&] { + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadBusy); + }).join(); + + Require(_Mtx_unlock_nid_postfix(&recMutex) == threadSuccess); + std::thread([&] { + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadBusy); + }).join(); + + Require(_Mtx_unlock_nid_postfix(&recMutex) == threadSuccess); + + std::thread([&] { + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadSuccess); + Require(_Mtx_unlock_nid_postfix(&recMutex) == threadSuccess); + }).join(); + + _Mtx_destroy_nid_postfix(&recMutex); +} + +void TestThrdSleep() { + timespec before{}; + Require(clock_gettime(CLOCK_REALTIME, &before) == 0); + + const std::int64_t sleepNanos = 20000000LL; + const std::int64_t targetTotal = static_cast(before.tv_sec) * nanosPerSecond + before.tv_nsec + sleepNanos; + const Xtime target{targetTotal / nanosPerSecond, targetTotal % nanosPerSecond}; + + Require(_Thrd_sleep_nid_postfix(&target, nullptr) == 0); + + timespec after{}; + Require(clock_gettime(CLOCK_REALTIME, &after) == 0); + const std::int64_t afterTotal = static_cast(after.tv_sec) * nanosPerSecond + after.tv_nsec; + Require(afterTotal >= targetTotal); + + const Xtime past{before.tv_sec - 1, before.tv_nsec}; + Require(_Thrd_sleep_nid_postfix(&past, nullptr) == 0); + + RequireThrows([] { + _Thrd_sleep_nid_postfix(nullptr, nullptr); + }); +} + +} + +int main() { + TestNearbyintf(); + TestMtxTrylock(); + TestThrdSleep(); + return 0; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 43a5c5397..a02256697 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -50,6 +50,9 @@ Throughout the project, every function at every stage either **does exactly what ### Unknown function info - [sceHttpUriBuild](../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) retains `//` for a non-opaque URI independently of the selected fields, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a8a70da595da2a96e0410309472ba4a7eb558144/src/core/libraries/network/http.cpp#L3551-L3558). Black Myth: Wukong builds its `Scheme` header with option 1 and expects `http://` or `https://`. Other partial field combinations follow the PS4 reference and remain unverified on PS5 hardware. +- [_Mtx_trylock](../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `k6pGNMwJB08`) - returns busy for a non-recursive mutex already owned by the caller, assumed to match `_Mtx_lock`'s ownership handling; exact console behavior is unverified. +- [nearbyintf](../../core/libs/prx/libc/src/Math.cpp) (libc, NID `c+4r-T-tEIc`) uses the host rounding mode, not the console's; console rounding mode state remains unverified. +- [_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. - [sceCoredumpGetThreadContextInfo](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `ShChva57wIM`) - declared as a throwing stub. The pointer and size arguments and integer return type follow [SharpProspero's PS5 bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs). The context structure and console behavior are unverified; the stub does not read or write caller memory. PPSA01736 imports it - [sceNpWebApi2PushEventAbortHandle](../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2, NID `1OLgvahaSco`) is a throwing stub. The two signed 32-bit handle arguments and return type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/np/np_web_api2/np_web_api2.cpp) (PS4); the PS5 signature and abort behavior are unverified. PPSA01736 imports it. - [sceNpGetSandboxInfo](../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `ZVZ++68nNI8`) - the name hashes to the imported NID (found with nidhunt, confirmed with `tools/nid_names.py`). STAR WARS: Galactic Racer (PPSA28416) imports it; its OnlineSubsystemPS5 calls it with one pointer to a 0x50-byte stack buffer and reads the buffer as a string (discussions/3086). The returned string, the exact parameter struct and console errors are unknown, so every call throws without reading or writing caller memory. A working implementation belongs with that title's bring-up. @@ -369,7 +372,7 @@ Throughout the project, every function at every stage either **does exactly what - [mkstemp and isatty](../../core/libs/prx/libScePosixForWebKit/Filesystem.cpp) (libScePosixForWebKit) follow FreeBSD's [temporary-file API](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/stdio/mktemp.c) and [terminal check](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/gen/isatty.c), without console verification. `mkstemp` replaces all trailing `X` characters, accepts a fixed name, and creates files through the guest path resolver with exclusive read/write access and mode 0600. Windows file permissions follow the existing kernel file implementation. `isatty` tests Windows console handles rather than all character devices; files, pipes and guest sockets return `ENOTTY`, closed descriptors return `EBADF`. - [arc4random](../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) returns four bytes from the same thread-local host `std::random_device` as `arc4random_buf`, uniform over the 32-bit range like FreeBSD 11.4's RC4 keystream in [arc4random.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/arc4random.c). The console's generator and its reseeding are unknown; `arc4random_uniform`, `arc4random_stir` and `arc4random_addrandom` are not in the module's export set and are absent - [fnmatch](../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) follows FreeBSD 11 [fnmatch.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/gen/fnmatch.c) in the C locale: each byte is one character, `FNM_CASEFOLD` folds ASCII letters only, and bracket ranges compare byte values. `[:class:]` names are not character classes: as in FreeBSD 11 through 14, `[[:digit:]]` is the set of `[`, `:`, `d`, `i`, `g`, `t` followed by a literal `]` (classes were added to `fnmatch.c` after 14). Multibyte characters and locale collation are not modelled. A null pattern or string throws -- [Dinkumware thread exports](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`, `xtime.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. `_Thrd_t` is taken to be the guest `pthread_t`, as the `std::_Pad::_Launch(pthread**)` export suggests: `_Thrd_join` returns `_Thrd_error` (4) when `pthread_join` fails and stores the thread's return value truncated to `int`. `_Thrd_sleep` reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep`, 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. `_Thrd_join` (32-bit store of the result, 0 or 4) and the spin lock (byte exchange loop, no yield) match that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning +- [Dinkumware thread exports](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. `_Thrd_t` is taken to be the guest `pthread_t`, as the `std::_Pad::_Launch(pthread**)` export suggests: `_Thrd_join` returns `_Thrd_error` (4) when `pthread_join` fails and stores the thread's return value truncated to `int`. `_Thrd_join` (32-bit store of the result, 0 or 4) and the spin lock (byte exchange loop, no yield) match that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning. - [_Cnd_signal, _Cnd_init_with_name](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - take a pointer to the handle slot like the libc `_Cnd_*` exports from [#3079](https://github.com/boykopovar/AnyPS5/pull/3079) and act on the same conditions. `_Cnd_signal` wakes one waiter and returns `_Thrd_success` (0), as `_Cnd_broadcast` does; whether the console maps a failing internal signal to `_Thrd_error` is not known. The signature `_Cnd_init_with_name(_Cnd_t*, const char* name)` is inferred from [fpPS4 `ps4_mtx_internal.pas`](https://github.com/red-prig/fpPS4/blob/trunk/src/libcinternal/ps4_mtx_internal.pas), where the PS4 `_Mtx_init` is `_Mtx_init_with_name` with the name `SceLibcI_Mtx` and the name only reaches the pthread object; here it is `_Cnd_init` and the name, which may be null, is ignored. Neither is confirmed on a PS5 `libc.prx`. `_Thrd_id` still throws: microsoft/STL returns an `unsigned int` thread id, but whether the PS5 returns the `pthread_t` or a kernel thread id, and at what width, is not known - [libSceZlib](../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports one outstanding request, null-timeout waits and one result retrieval. Timed waits, multiple outstanding requests, repeated retrieval and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. - [Windows file timestamps](../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes` and `futimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond