From 092f7ef00afa4b1e7c692ab41d9865d51fe82799 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 14:56:06 +0200 Subject: [PATCH 1/2] feat(libkernel): implement the pthread spinlock calls Add pthread_spin_init (ZMn3clnAGBA), pthread_spin_destroy (IJIggoPZExk), pthread_spin_lock (pw+70ClLYlY), pthread_spin_trylock (rCTGkBIHfPY) and pthread_spin_unlock (LEfMMCT+SlM). The NIDs were computed with tools/nid_names.py and match aerolib.csv. libkernel has no scePthreadSpin* twin in aerolib, so the calls are implemented directly, like the barrier. Titles: Porpoise PPSA99764 and RetroArch PPSA99169 import pthread_spin_lock. Predicted from relink registry order, not observed: after pthread_cond_reltimedwait_np, Porpoise stops with "FAIL: unresolved ELF import pw+70ClLYlY from libkernel.prx" (registry index 387; RetroArch index 493). No call site is resolved in the registry. Behaviour follows FreeBSD 11.4 libthr thr_pspinlock.c and thr_umtx.h (8c639945a44588f6cfdddfb057bb5aad88735da0). The guest pthread_spinlock_t holds a pointer to an allocated lock. EINVAL for a null or destroyed lock and for a pshared other than PTHREAD_PROCESS_PRIVATE or PTHREAD_PROCESS_SHARED, EBUSY from trylock while any thread holds the lock, EPERM from unlock by a thread that does not hold it. libthr spins forever when the holder locks again; that call throws here. A shared lock is allocated like a private one, since guest code runs in one process. Recorded in TechnicalDebt. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 13ebf7f926fa2ee1c825cb1fcf18451a8781c045) --- core/libs/prx/libkernel/CMakeLists.txt | 11 ++ .../prx/libkernel/Pthread/Posix/Spinlock.cpp | 76 +++++++++++ core/libs/tests/GuestPthreadSpin.cpp | 122 ++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 210 insertions(+) create mode 100644 core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp create mode 100644 core/libs/tests/GuestPthreadSpin.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 6fa23e5fe..c3e10c731 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -36,6 +36,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Once.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Tsd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Sem.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Spinlock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Socket.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Udp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/AddressConversion.cpp @@ -314,6 +315,16 @@ if(BUILD_TESTING) set_tests_properties(guest_thread_self PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_spin_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadSpin.cpp) + target_include_directories(guest_pthread_spin_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_spin_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_spin_tests) + add_test(NAME guest_pthread_spin COMMAND guest_pthread_spin_tests) + set_tests_properties(guest_pthread_spin PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_spin PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_getname_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadGetname.cpp) target_include_directories(guest_pthread_getname_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_getname_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp b/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp new file mode 100644 index 000000000..3990f0d29 --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp @@ -0,0 +1,76 @@ +#include "SceTypes.hpp" +#include "Common.hpp" +#include +#include +#include +#include + +struct PthreadSpinlockPrivate { + std::atomic owner{}; +}; + +namespace { + +constexpr int GUEST_EPERM = 1; +constexpr int GUEST_ENOMEM = 12; +constexpr int GUEST_EBUSY = 16; +constexpr int PROCESS_PRIVATE = 0; +constexpr int PROCESS_SHARED = 1; +constexpr int SPIN_COUNT = 100000; + +bool tryAcquire(PthreadSpinlockPrivate* spinlock) { + std::thread::id unowned{}; + return spinlock->owner.compare_exchange_strong(unowned, std::this_thread::get_id(), std::memory_order_acquire, std::memory_order_relaxed); +} + +} + +extern "C" { + +int APS5_VABI pthread_spin_init_nid_postfix(PthreadSpinlockPrivate** lock, int pshared) { + if (!lock) return PosixThread::GUEST_EINVAL; + if (pshared != PROCESS_PRIVATE && pshared != PROCESS_SHARED) return PosixThread::GUEST_EINVAL; + auto* spinlock = new (std::nothrow) PthreadSpinlockPrivate; + if (!spinlock) return GUEST_ENOMEM; + *lock = spinlock; + return 0; +} + +int APS5_VABI pthread_spin_destroy_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + delete *lock; + *lock = nullptr; + return 0; +} + +int APS5_VABI pthread_spin_trylock_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + return tryAcquire(*lock) ? 0 : GUEST_EBUSY; +} + +int APS5_VABI pthread_spin_lock_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + auto* spinlock = *lock; + if (spinlock->owner.load(std::memory_order_relaxed) == std::this_thread::get_id()) + throw std::runtime_error("pthread_spin_lock: the calling thread already holds the lock and would spin forever"); + int count = SPIN_COUNT; + while (!tryAcquire(spinlock)) { + while (spinlock->owner.load(std::memory_order_relaxed) != std::thread::id{}) { + if (--count <= 0) { + count = SPIN_COUNT; + std::this_thread::yield(); + } + } + } + return 0; +} + +int APS5_VABI pthread_spin_unlock_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + auto owner = std::this_thread::get_id(); + if (!(*lock)->owner.compare_exchange_strong(owner, std::thread::id{}, std::memory_order_release, std::memory_order_relaxed)) + return GUEST_EPERM; + return 0; +} + +} diff --git a/core/libs/tests/GuestPthreadSpin.cpp b/core/libs/tests/GuestPthreadSpin.cpp new file mode 100644 index 000000000..9783df2dd --- /dev/null +++ b/core/libs/tests/GuestPthreadSpin.cpp @@ -0,0 +1,122 @@ +#include "SceTypes.hpp" +#include +#include + +struct PthreadSpinlockPrivate; +using PthreadSpinlock = PthreadSpinlockPrivate*; + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI pthread_spin_init_nid_postfix(PthreadSpinlock* lock, int pshared); +int APS5_VABI pthread_spin_destroy_nid_postfix(PthreadSpinlock* lock); +int APS5_VABI pthread_spin_lock_nid_postfix(PthreadSpinlock* lock); +int APS5_VABI pthread_spin_trylock_nid_postfix(PthreadSpinlock* lock); +int APS5_VABI pthread_spin_unlock_nid_postfix(PthreadSpinlock* lock); +} + +static constexpr int GUEST_EPERM = 1; +static constexpr int GUEST_EBUSY = 16; +static constexpr int GUEST_EINVAL = 22; +static constexpr int PTHREAD_PROCESS_PRIVATE = 0; +static constexpr int PTHREAD_PROCESS_SHARED = 1; +static constexpr int Increments = 200000; + +static void Require(bool value) { if (!value) std::abort(); } + +struct Shared { + PthreadSpinlock lock = nullptr; + long counter = 0; + int trylock = -1; + int unlock = -1; +}; + +static void* APS5_VABI Contender(void* arg) { + auto& shared = *static_cast(arg); + shared.trylock = pthread_spin_trylock_nid_postfix(&shared.lock); + shared.unlock = pthread_spin_unlock_nid_postfix(&shared.lock); + return nullptr; +} + +static void* APS5_VABI Incrementer(void* arg) { + auto& shared = *static_cast(arg); + for (int i = 0; i < Increments; ++i) { + Require(pthread_spin_lock_nid_postfix(&shared.lock) == 0); + ++shared.counter; + Require(pthread_spin_unlock_nid_postfix(&shared.lock) == 0); + } + return nullptr; +} + +static void ArgumentErrors() { + Require(pthread_spin_init_nid_postfix(nullptr, PTHREAD_PROCESS_PRIVATE) == GUEST_EINVAL); + PthreadSpinlock lock = nullptr; + Require(pthread_spin_init_nid_postfix(&lock, 2) == GUEST_EINVAL); + Require(pthread_spin_init_nid_postfix(&lock, -1) == GUEST_EINVAL); + Require(lock == nullptr); + Require(pthread_spin_lock_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_trylock_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_unlock_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_destroy_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_lock_nid_postfix(&lock) == GUEST_EINVAL); + Require(pthread_spin_trylock_nid_postfix(&lock) == GUEST_EINVAL); + Require(pthread_spin_unlock_nid_postfix(&lock) == GUEST_EINVAL); + Require(pthread_spin_destroy_nid_postfix(&lock) == GUEST_EINVAL); +} + +static void SingleThread(int pshared) { + PthreadSpinlock lock = nullptr; + Require(pthread_spin_init_nid_postfix(&lock, pshared) == 0); + Require(lock != nullptr); + Require(pthread_spin_unlock_nid_postfix(&lock) == GUEST_EPERM); + Require(pthread_spin_lock_nid_postfix(&lock) == 0); + Require(pthread_spin_trylock_nid_postfix(&lock) == GUEST_EBUSY); + bool threw = false; + try { + pthread_spin_lock_nid_postfix(&lock); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + Require(pthread_spin_unlock_nid_postfix(&lock) == 0); + Require(pthread_spin_unlock_nid_postfix(&lock) == GUEST_EPERM); + Require(pthread_spin_trylock_nid_postfix(&lock) == 0); + Require(pthread_spin_unlock_nid_postfix(&lock) == 0); + Require(pthread_spin_destroy_nid_postfix(&lock) == 0); + Require(lock == nullptr); + Require(pthread_spin_destroy_nid_postfix(&lock) == GUEST_EINVAL); +} + +static void HeldByAnotherThread() { + Shared shared; + Require(pthread_spin_init_nid_postfix(&shared.lock, PTHREAD_PROCESS_PRIVATE) == 0); + Require(pthread_spin_lock_nid_postfix(&shared.lock) == 0); + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Contender, &shared, nullptr) == 0); + Require(scePthreadJoin(thread, nullptr) == 0); + Require(shared.trylock == GUEST_EBUSY); + Require(shared.unlock == GUEST_EPERM); + Require(pthread_spin_unlock_nid_postfix(&shared.lock) == 0); + Require(pthread_spin_destroy_nid_postfix(&shared.lock) == 0); +} + +static void MutualExclusion() { + Shared shared; + Require(pthread_spin_init_nid_postfix(&shared.lock, PTHREAD_PROCESS_PRIVATE) == 0); + Pthread first = nullptr; + Pthread second = nullptr; + Require(scePthreadCreate(&first, nullptr, Incrementer, &shared, nullptr) == 0); + Require(scePthreadCreate(&second, nullptr, Incrementer, &shared, nullptr) == 0); + Require(scePthreadJoin(first, nullptr) == 0); + Require(scePthreadJoin(second, nullptr) == 0); + Require(shared.counter == 2L * Increments); + Require(pthread_spin_destroy_nid_postfix(&shared.lock) == 0); +} + +int main() { + ArgumentErrors(); + SingleThread(PTHREAD_PROCESS_PRIVATE); + SingleThread(PTHREAD_PROCESS_SHARED); + HeldByAnotherThread(); + MutualExclusion(); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 6fde01523..f659bc491 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -625,3 +625,4 @@ Throughout the project, every function at every stage either **does exactly what - [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. - [sceAgcCreatePrimState](../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well +- [pthread_spin_init, pthread_spin_destroy, pthread_spin_lock, pthread_spin_trylock, pthread_spin_unlock](../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) follow FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) and the owner checks of its [`thr_umtx.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_umtx.h#L80-L175), unverified on the console: `EINVAL` for a null or destroyed lock and a `pshared` other than 0 or 1, `EBUSY` from `trylock` while any thread holds it, `EPERM` from `unlock` by a thread that does not hold it. `PTHREAD_PROCESS_SHARED` allocates a private lock, as guest code runs in one process. libthr spins forever when the holder locks again; here that call throws From 774f21845dff20abddcdb02dabd4b9a6192c8ce3 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 14:58:03 +0200 Subject: [PATCH 2/2] feat(libkernel): implement pthread_cond_reltimedwait_np Add pthread_cond_reltimedwait_np (K953PF5u6Pc; computed with tools/nid_names.py and matching aerolib.csv) as scePthreadCondTimedwait with POSIX error numbers: ETIMEDOUT when the relative timeout expires, EPERM when the caller does not hold the mutex, and the mutex held again on return in every waiting case. Titles: Porpoise PPSA99764 and RetroArch PPSA99169 import it. Predicted from relink registry order, not observed: Porpoise stops with "FAIL: unresolved ELF import K953PF5u6Pc from libkernel.prx" (registry index 384; RetroArch index 490). No call site is resolved in the registry. The third argument is a relative timeout in microseconds, not a struct timespec. FreeBSD 11.4 libthr (8c639945a44588f6cfdddfb057bb5aad88735da0) has no such function. shadPS4 (3a9f57265d28975658f1ca941daa5c879c7a74bd, src/core/libraries/kernel/threads/condvar.cpp:229-234, 445, 467-468) registers posix_pthread_cond_reltimedwait_np(cond, mutex, u64 usec) for this NID and the same function for scePthreadCondTimedwait (BmMjYxmew1w), whose microsecond argument this repository already implements. The width follows the SCE twin's 32-bit SceKernelUseconds, as sem_reltimedwait_np does, not shadPS4's u64. There is no timespec to validate, so no EINVAL case. Recorded in TechnicalDebt. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit c86ec8417d12f5b12136b0f06eae06493b0e7a62) --- .../libs/prx/libkernel/Pthread/Posix/Cond.cpp | 4 ++ core/libs/tests/GuestCondTimedwait.cpp | 67 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 72 insertions(+) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp index b9a7b211c..5c5e64d18 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp @@ -41,6 +41,10 @@ int APS5_VABI pthread_cond_timedwait_nid_postfix(PthreadCond* cond, PthreadMutex return toPosix(CondOperations::AbsoluteTimedwait(cond, mutex, abstime)); } +int APS5_VABI pthread_cond_reltimedwait_np_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, KernelUseconds usec) { + return toPosix(scePthreadCondTimedwait(cond, mutex, usec)); +} + int APS5_VABI pthread_cond_wait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex) { return toPosix(scePthreadCondWait(cond, mutex)); } diff --git a/core/libs/tests/GuestCondTimedwait.cpp b/core/libs/tests/GuestCondTimedwait.cpp index 310812871..5a9190e1e 100644 --- a/core/libs/tests/GuestCondTimedwait.cpp +++ b/core/libs/tests/GuestCondTimedwait.cpp @@ -1,5 +1,6 @@ #include "SceTypes.hpp" #include +#include #include #include @@ -10,9 +11,12 @@ int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* a int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex); int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char* name); int APS5_VABI scePthreadCondDestroy(PthreadCond* cond); int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, unsigned int usec); +int APS5_VABI scePthreadCondSignal(PthreadCond* cond); +int APS5_VABI pthread_cond_reltimedwait_np_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, unsigned int usec); int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *)(void*), void*, void*); void APS5_VABI __pthread_cxa_finalize_nid_postfix(void*); unsigned int APS5_VABI sceKernelSleep(unsigned int seconds); @@ -23,6 +27,9 @@ static void APS5_VABI Cleanup(void*) { cleanupAt = 0; } static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); +static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); +static constexpr int POSIX_EPERM = 1; +static constexpr int POSIX_ETIMEDOUT = 60; static void Require(bool value) { if (!value) std::abort(); } @@ -39,7 +46,67 @@ static void* APS5_VABI Contender(void* arg) { return nullptr; } +struct Signalled { + PthreadMutex mutex = nullptr; + PthreadCond cond = nullptr; + bool ready = false; + int trylock = SCE_OK; +}; + +static void* APS5_VABI Trylocker(void* arg) { + auto& signalled = *static_cast(arg); + signalled.trylock = scePthreadMutexTrylock(&signalled.mutex); + return nullptr; +} + +static void* APS5_VABI Signaller(void* arg) { + auto& signalled = *static_cast(arg); + Require(scePthreadMutexLock(&signalled.mutex) == SCE_OK); + signalled.ready = true; + Require(scePthreadCondSignal(&signalled.cond) == SCE_OK); + Require(scePthreadMutexUnlock(&signalled.mutex) == SCE_OK); + return nullptr; +} + +static void RequireHeldByCaller(Signalled& signalled) { + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Trylocker, &signalled, nullptr) == SCE_OK); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + Require(signalled.trylock == SCE_KERNEL_ERROR_EBUSY); +} + +static void RelativeWait() { + Signalled signalled; + Require(scePthreadMutexInit(&signalled.mutex, nullptr, nullptr) == SCE_OK); + Require(scePthreadCondInit(&signalled.cond, nullptr, nullptr) == SCE_OK); + + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 1000) == POSIX_EPERM); + + Require(scePthreadMutexLock(&signalled.mutex) == SCE_OK); + const auto start = std::chrono::steady_clock::now(); + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 50000) == POSIX_ETIMEDOUT); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40)); + RequireHeldByCaller(signalled); + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 0) == POSIX_ETIMEDOUT); + RequireHeldByCaller(signalled); + + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Signaller, &signalled, nullptr) == SCE_OK); + const auto waitStart = std::chrono::steady_clock::now(); + while (!signalled.ready) + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 10000000) == 0); + Require(std::chrono::steady_clock::now() - waitStart < std::chrono::seconds(5)); + RequireHeldByCaller(signalled); + Require(scePthreadMutexUnlock(&signalled.mutex) == SCE_OK); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + + Require(scePthreadCondDestroy(&signalled.cond) == SCE_OK); + Require(scePthreadMutexDestroy(&signalled.mutex) == SCE_OK); +} + int main() { + RelativeWait(); + Context context; PthreadCond cond = nullptr; Require(scePthreadMutexInit(&context.mutex, nullptr, nullptr) == SCE_OK); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index f659bc491..ab0e8c98a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -626,3 +626,4 @@ Throughout the project, every function at every stage either **does exactly what - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. - [sceAgcCreatePrimState](../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well - [pthread_spin_init, pthread_spin_destroy, pthread_spin_lock, pthread_spin_trylock, pthread_spin_unlock](../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) follow FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) and the owner checks of its [`thr_umtx.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_umtx.h#L80-L175), unverified on the console: `EINVAL` for a null or destroyed lock and a `pshared` other than 0 or 1, `EBUSY` from `trylock` while any thread holds it, `EPERM` from `unlock` by a thread that does not hold it. `PTHREAD_PROCESS_SHARED` allocates a private lock, as guest code runs in one process. libthr spins forever when the holder locks again; here that call throws +- [pthread_cond_reltimedwait_np](../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) takes `(cond, mutex, usec)`, a relative timeout in microseconds, and is `scePthreadCondTimedwait` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadCondTimedwait` ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/condvar.cpp#L229-L234)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `sem_reltimedwait_np`. Unverified on the console