diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index c2c1cc811..fb835934f 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -234,6 +234,8 @@ static std::atomic_flag sharedPtrSpinLock; extern "C" { int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** value); int APS5_VABI wcsrtombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t** source, std::size_t limit, void* state); +int APS5_VABI _Cnd_init_nid_postfix(void** handle); +int SignalCondition_nid_no_patch(void** handle); } static KernelTimespec XtimeDeadline(const Xtime& time) { @@ -895,18 +897,16 @@ int APS5_VABI _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_n return 0; } -int APS5_VABI _Cnd_signal_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI _Cnd_signal_nid_postfix(void** handle) { + return SignalCondition_nid_no_patch(handle); } void APS5_VABI _Unlock_shared_ptr_spin_lock_nid_postfix(void) { sharedPtrSpinLock.clear(std::memory_order_release); } -int APS5_VABI _Cnd_init_with_name_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI _Cnd_init_with_name_nid_postfix(void** handle, const char*) { + return _Cnd_init_nid_postfix(handle); } int APS5_VABI _ZTVN10__cxxabiv120__function_type_infoE_nid_postfix(void) { diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index 5399fd351..4565d8331 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -173,4 +173,9 @@ int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle) { return threadSuccess; } +int SignalCondition_nid_no_patch(void** handle) { + ResolveCondition(handle, "_Cnd_signal")->native.notify_one(); + return threadSuccess; +} + } diff --git a/core/libs/tests/GuestAmprThreads.cpp b/core/libs/tests/GuestAmprThreads.cpp index 81ea82946..6fa491548 100644 --- a/core/libs/tests/GuestAmprThreads.cpp +++ b/core/libs/tests/GuestAmprThreads.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include #include @@ -24,6 +25,16 @@ int APS5_VABI wcstombs_s_nid_postfix(std::size_t* result, char* destination, std int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI pthread_detach_nid_postfix(Pthread thread); int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp); +int APS5_VABI _Cnd_signal_nid_postfix(void** handle); +int APS5_VABI _Cnd_init_with_name_nid_postfix(void** handle, const char* name); +int APS5_VABI _Cnd_init_nid_postfix(void** handle); +void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); +int APS5_VABI _Cnd_wait_nid_postfix(void** condition, void** mutex); +int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle); +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); } static constexpr int ThrdSuccess = 0; @@ -197,10 +208,119 @@ static void TestWcstombs() { Require(result == Failed); } +template +static void RequireThrows(TCall call) { + try { + call(); + } catch (const TException&) { + return; + } + std::abort(); +} + +struct SignalContext { + void* condition = nullptr; + void* mutex = nullptr; + int waiting = 0; + int tokens = 0; + int consumed = 0; + int wakeups = 0; +}; + +static void Lock(SignalContext& context) { + Require(_Mtx_lock_nid_postfix(&context.mutex) == ThrdSuccess); +} + +static void Unlock(SignalContext& context) { + Require(_Mtx_unlock_nid_postfix(&context.mutex) == ThrdSuccess); +} + +static void* APS5_VABI ConsumeToken(void* arg) { + auto& context = *static_cast(arg); + Lock(context); + ++context.waiting; + while (context.tokens == 0) { + Require(_Cnd_wait_nid_postfix(&context.condition, &context.mutex) == ThrdSuccess); + ++context.wakeups; + } + --context.tokens; + ++context.consumed; + Unlock(context); + return nullptr; +} + +template +static void WaitUntil(SignalContext& context, TPredicate predicate) { + for (;;) { + Lock(context); + const bool done = predicate(); + Unlock(context); + if (done) return; + std::this_thread::yield(); + } +} + +static void Signal(SignalContext& context) { + Lock(context); + ++context.tokens; + Require(_Cnd_signal_nid_postfix(&context.condition) == ThrdSuccess); + Unlock(context); +} + +static void TestConditionSignal() { + SignalContext context; + context.condition = reinterpret_cast(std::uintptr_t{1}); + Require(_Cnd_init_with_name_nid_postfix(&context.condition, "AmprSignal") == ThrdSuccess && context.condition != nullptr); + Require(_Mtx_init_nid_postfix(&context.mutex, 0) == ThrdSuccess); + Require(_Cnd_signal_nid_postfix(&context.condition) == ThrdSuccess); + + const auto single = Start(ConsumeToken, &context); + WaitUntil(context, [&] { return context.waiting == 1; }); + Signal(context); + Require(_Thrd_join_nid_postfix(single, nullptr) == ThrdSuccess); + Require(context.consumed == 1 && context.tokens == 0); + + context.waiting = 0; + context.consumed = 0; + context.wakeups = 0; + const std::array pair{Start(ConsumeToken, &context), Start(ConsumeToken, &context)}; + WaitUntil(context, [&] { return context.waiting == 2; }); + Signal(context); + WaitUntil(context, [&] { return context.consumed == 1; }); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Lock(context); + Require(context.consumed == 1 && context.wakeups == 1 && context.tokens == 0); + Unlock(context); + Signal(context); + for (auto thread : pair) Require(_Thrd_join_nid_postfix(thread, nullptr) == ThrdSuccess); + Require(context.consumed == 2 && context.tokens == 0); + Require(_Cnd_broadcast_nid_postfix(&context.condition) == ThrdSuccess); + + RequireThrows([] { _Cnd_signal_nid_postfix(nullptr); }); + void* empty = nullptr; + RequireThrows([&] { _Cnd_signal_nid_postfix(&empty); }); + RequireThrows([&] { _Cnd_signal_nid_postfix(&context.mutex); }); + RequireThrows([] { _Cnd_init_with_name_nid_postfix(nullptr, "AmprSignal"); }); + _Cnd_destroy_nid_postfix(&context.condition); + Require(context.condition == nullptr); + RequireThrows([&] { _Cnd_signal_nid_postfix(&context.condition); }); + + void* unnamed = nullptr; + Require(_Cnd_init_with_name_nid_postfix(&unnamed, nullptr) == ThrdSuccess && unnamed != nullptr); + Require(_Cnd_signal_nid_postfix(&unnamed) == ThrdSuccess); + _Cnd_destroy_nid_postfix(&unnamed); + void* plain = nullptr; + Require(_Cnd_init_nid_postfix(&plain) == ThrdSuccess); + Require(_Cnd_signal_nid_postfix(&plain) == ThrdSuccess); + _Cnd_destroy_nid_postfix(&plain); + _Mtx_destroy_nid_postfix(&context.mutex); +} + int main() { TestThreadJoin(); TestThreadSleep(); TestSpinLock(); TestWcstombs(); + TestConditionSignal(); return 0; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b72e13228..074515d17 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -347,6 +347,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceOpusDec](../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for 48000 Hz mono/stereo signed-16 decoding. The six signatures, 640-byte opaque state allocation and byte-count return follow [public PS5 decoder research](https://github.com/blackbearreloaded/ps5-hardware-audio-decoding/blob/26be5a12f92b2cdc67f8a9e082fd69ae0e9a35a2/docs/DECODING.md#direct-opus-abi) and [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). Console PCM equivalence and error codes are unverified. Other sample rates, packet loss concealment, invalid inputs and termination with live decoders throw. Context identifiers and host decoder state are private registries; the firmware's opaque state layout and error recovery are unknown. - [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`. - [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 +- [_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 - [fchdir](../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) changes the guest working directory to the descriptor's directory: on Windows through the path recorded when the descriptor was opened, on Linux through the `/proc/self/fd` link target. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. FreeBSD `change_dir`'s search-permission check (`EACCES`) is not modelled, and a directory renamed after it was opened is still reached through the recorded path. Console behaviour is unverified