From d991cdccf9841d5fdd2290e9730b8995772de315 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 10:39:55 +0200 Subject: [PATCH 1/4] feat(libSceAmpr): implement the std::_Pad thread launch helpers Implement _ZNSt4_PadC2Ev, _ZNSt4_PadD2Ev, _ZNSt4_Pad7_LaunchEPP7pthread and _ZNSt4_Pad8_ReleaseEv, which Deathloop (PPSA01670) and Minecraft Legends (PPSA05510) import from libSceAmpr for the std::thread launch path. The algorithm and object layout follow Dinkumware V6.00 thr/xthread and thr/threadcall.cpp of Visual C++ 2013 (ganboing/crt.vc12 f1d47126): a vtable pointer whose first slot is the pure virtual _Go(), then the condition and mutex handles and bool _Started (0x8, 0x10, 0x18), with the console's pointer-to-slot handles of libc Threads.cpp (#3079). The pad uses libc's _Cnd_*/_Mtx_* exports, starts the guest thread with pthread_create and an entry that calls _Go() and returns its result as the exit code, as _Call_func does. The unverified layout, thread attributes and error reporting are recorded in TechnicalDebt.md. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libSceAmpr/Export.cpp | 74 ++++++++++++--- core/libs/tests/GuestAmprThreads.cpp | 131 +++++++++++++++++++++++++++ docs/dev/debt/libSceAmpr.md | 1 + 3 files changed, 194 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index f5e5f01a6..e48a0dae1 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include static constexpr int SCE_AMPR_ERROR_BUFFER_FULL = 0x8002001C; @@ -226,6 +227,8 @@ static int RecordWriteAddressFromCounterPair(Apr::CommandBufferObject* buffer, v static constexpr int ThrdSuccess = 0; static constexpr int ThrdError = 4; +static constexpr int MtxPlain = 0x1; + static std::atomic_flag sharedPtrSpinLock; struct AmmUsageStatsData { @@ -240,14 +243,42 @@ static_assert(sizeof(AmmUsageStatsData) == 0x18); static constexpr std::uint16_t AmmPageTablePoolEntries = 512; static constexpr std::uint32_t AmmRingIdleFlags = 0x7; +struct GuestPad { + unsigned (APS5_VABI* const* vtable)(GuestPad*); + void* condition; + void* mutex; + bool started; +}; + +static_assert(offsetof(GuestPad, condition) == 0x8 && offsetof(GuestPad, mutex) == 0x10 && offsetof(GuestPad, started) == 0x18 && sizeof(GuestPad) == 0x20); + extern "C" { +int APS5_VABI pthread_create_nid_postfix(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg); int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** value); +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 _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 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); [[noreturn]] void ThrowCppError_nid_no_patch(int code); } +static void RequireThrd(int result, const char* what) { + if (result != ThrdSuccess) throw std::runtime_error(std::string(what) + " failed with " + std::to_string(result)); +} + +static void* APS5_VABI PadEntry(void* argument) { + auto* pad = static_cast(argument); + const auto go = pad->vtable[0]; + return reinterpret_cast(static_cast(go(pad))); +} + extern "C" { int APS5_VABI sceAmprCommandBufferWriteAddressOnCompletion(Apr::CommandBufferObject* buffer, volatile std::uint64_t* address, std::uint64_t value) { @@ -865,19 +896,36 @@ int APS5_VABI _Thrd_join_nid_postfix(Pthread thread, int* code) { return ThrdSuccess; } -int APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad) { + if (!pad) APS5_INVALID_ARG_EX; + RequireThrd(_Mtx_lock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_lock"); + pad->started = true; + RequireThrd(_Cnd_broadcast_nid_postfix(&pad->condition), "_Pad::_Release: _Cnd_broadcast"); + RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_unlock"); } -int APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(GuestPad* pad) { + if (!pad) APS5_INVALID_ARG_EX; + RequireThrd(_Cnd_init_nid_postfix(&pad->condition), "_Pad::_Pad: _Cnd_init"); + const int created = _Mtx_init_nid_postfix(&pad->mutex, MtxPlain); + if (created != ThrdSuccess) { + _Cnd_destroy_nid_postfix(&pad->condition); + RequireThrd(created, "_Pad::_Pad: _Mtx_init"); + } + pad->started = false; + const int locked = _Mtx_lock_nid_postfix(&pad->mutex); + if (locked != ThrdSuccess) { + _Mtx_destroy_nid_postfix(&pad->mutex); + _Cnd_destroy_nid_postfix(&pad->condition); + RequireThrd(locked, "_Pad::_Pad: _Mtx_lock"); + } } -int APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(GuestPad* pad) { + if (!pad) APS5_INVALID_ARG_EX; + RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::~_Pad: _Mtx_unlock"); + _Mtx_destroy_nid_postfix(&pad->mutex); + _Cnd_destroy_nid_postfix(&pad->condition); } int APS5_VABI _Thrd_id_nid_postfix(void) { @@ -885,9 +933,11 @@ int APS5_VABI _Thrd_id_nid_postfix(void) { return 0; } -int APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, Pthread* thread) { + if (!pad || !thread) APS5_INVALID_ARG_EX; + const int created = pthread_create_nid_postfix(thread, nullptr, PadEntry, pad); + if (created != 0) throw std::runtime_error("_Pad::_Launch: pthread_create failed with " + std::to_string(created)); + while (!pad->started) RequireThrd(_Cnd_wait_nid_postfix(&pad->condition, &pad->mutex), "_Pad::_Launch: _Cnd_wait"); } int APS5_VABI _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix(void) { diff --git a/core/libs/tests/GuestAmprThreads.cpp b/core/libs/tests/GuestAmprThreads.cpp index 6fa491548..ea2e2e520 100644 --- a/core/libs/tests/GuestAmprThreads.cpp +++ b/core/libs/tests/GuestAmprThreads.cpp @@ -16,7 +16,27 @@ struct GuestXtime { std::int64_t nsec; }; +struct PadTarget { + std::atomic* order; + std::atomic* destroyed; + int beforeRelease; + int afterRelease; + unsigned exitCode; +}; + +struct TestPad { + unsigned (APS5_VABI* const* vtable)(TestPad*); + void* condition; + void* mutex; + bool started; + PadTarget target; +}; + extern "C" { +void APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(TestPad* pad); +void APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(TestPad* pad); +void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(TestPad* pad, Pthread* thread); +void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(TestPad* pad); int APS5_VABI _Thrd_join_nid_postfix(Pthread thread, int* code); int APS5_VABI _Thrd_sleep_nid_postfix(const GuestXtime* target, GuestXtime* remaining); void APS5_VABI _Lock_shared_ptr_spin_lock_nid_postfix(void); @@ -316,7 +336,118 @@ static void TestConditionSignal() { _Mtx_destroy_nid_postfix(&context.mutex); } +static unsigned APS5_VABI PadGo(TestPad* pad) { + const auto local = pad->target; + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + local.order->store(local.beforeRelease); + _ZNSt4_Pad8_ReleaseEv_nid_postfix(pad); + while (!local.destroyed->load()) std::this_thread::yield(); + local.order->store(local.afterRelease); + return local.exitCode; +} + +static constexpr std::array PadVtable{PadGo}; + +template +static bool Throws(TCall call) { + try { + call(); + } catch (const TException&) { + return true; + } + return false; +} + +static void ConstructPad(TestPad& pad, std::atomic& order, std::atomic& destroyed, int index, unsigned exitCode) { + std::memset(&pad, 0xCD, sizeof(pad)); + pad.vtable = PadVtable.data(); + _ZNSt4_PadC2Ev_nid_postfix(&pad); + Require(pad.vtable == PadVtable.data()); + Require(pad.condition && pad.mutex && !pad.started); + pad.target = {&order, &destroyed, 2 * index + 1, 2 * index + 2, exitCode}; +} + +static void TestPadLaunch() { + std::atomic order{0}; + std::atomic destroyed{false}; + TestPad pad; + ConstructPad(pad, order, destroyed, 0, 0); + Pthread thread = nullptr; + _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(&pad, &thread); + Require(thread != nullptr); + Require(pad.started); + Require(order.load() == 1); + _ZNSt4_PadD2Ev_nid_postfix(&pad); + Require(!pad.condition && !pad.mutex); + Require(order.load() == 1); + destroyed = true; + int code = -1; + Require(_Thrd_join_nid_postfix(thread, &code) == ThrdSuccess); + Require(code == 0); + Require(order.load() == 2); +} + +static void TestPadLaunchMany() { + constexpr int Count = 4; + std::array, Count> orders{}; + std::atomic destroyed{false}; + std::array pads{}; + std::array threads{}; + for (int index = 0; index < Count; ++index) { + ConstructPad(pads[index], orders[index], destroyed, index, 100u + index); + _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(&pads[index], &threads[index]); + Require(orders[index].load() == 2 * index + 1); + _ZNSt4_PadD2Ev_nid_postfix(&pads[index]); + } + destroyed = true; + for (int index = 0; index < Count; ++index) { + int code = 0; + Require(_Thrd_join_nid_postfix(threads[index], &code) == ThrdSuccess); + Require(code == 100 + index); + Require(orders[index].load() == 2 * index + 2); + } +} + +struct ForeignDestroy { + TestPad* pad; + bool rejected; +}; + +static void* APS5_VABI DestroyPad(void* arg) { + auto& context = *static_cast(arg); + context.rejected = Throws([&] { _ZNSt4_PadD2Ev_nid_postfix(context.pad); }); + return nullptr; +} + +static void TestPadErrors() { + Require(Throws([] { _ZNSt4_PadC2Ev_nid_postfix(nullptr); })); + Require(Throws([] { _ZNSt4_PadD2Ev_nid_postfix(nullptr); })); + Require(Throws([] { _ZNSt4_Pad8_ReleaseEv_nid_postfix(nullptr); })); + Pthread thread = nullptr; + Require(Throws([&] { _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(nullptr, &thread); })); + + std::atomic order{0}; + std::atomic destroyed{true}; + TestPad pad; + ConstructPad(pad, order, destroyed, 0, 0); + Require(Throws([&] { _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(&pad, nullptr); })); + Require(!pad.started && order.load() == 0); + Require(Throws([&] { _ZNSt4_Pad8_ReleaseEv_nid_postfix(&pad); })); + Require(!pad.started); + + ForeignDestroy context{&pad, false}; + Require(_Thrd_join_nid_postfix(Start(DestroyPad, &context), nullptr) == ThrdSuccess); + Require(context.rejected); + Require(pad.condition && pad.mutex); + _ZNSt4_PadD2Ev_nid_postfix(&pad); + Require(!pad.condition && !pad.mutex); + Require(Throws([&] { _ZNSt4_PadD2Ev_nid_postfix(&pad); })); +} + int main() { + TestPadLaunch(); + TestPadLaunchMany(); + TestPadErrors(); TestThreadJoin(); TestThreadSleep(); TestSpinLock(); diff --git a/docs/dev/debt/libSceAmpr.md b/docs/dev/debt/libSceAmpr.md index 3b088674e..ca3383747 100644 --- a/docs/dev/debt/libSceAmpr.md +++ b/docs/dev/debt/libSceAmpr.md @@ -13,3 +13,4 @@ - [sceAmprAmmGetUsageStatsData](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) fills the caller-sized 24-byte statistics structure with 512 page-table pool entries, no allocated entries, a zero high watermark and ring idle flags 0x7, the constants of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libAmpr.cpp); they are not measured on a console, and the real call is a pass-through to `sceKernelMapperGetUsageStatsData` per [drakmor/ampr_emu](https://github.com/drakmor/ampr_emu/blob/main/src/ampr_emu_amm.cpp), which this port has no kernel-side counterpart for. - [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 +- [std::_Pad](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follows Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` of Visual C++ 2013 ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. The pad is taken to be that header's class with the console's pointer handles: a vtable pointer whose first slot is the pure virtual `_Go()`, then `_Cnd_t` at 0x8, `_Mtx_t` at 0x10 and `bool _Started` at 0x18, 32 bytes, the derived `_LaunchPad` target following it; the console's `_Pad(const char*)` and `_Launch(const char*, pthread**)`/`_Launch(pthread_attr**, pthread**)` overloads suggest named primitives and could mean another layout or vtable. `_Launch` starts the thread with `pthread_create` and default attributes (the console's `_Thrd_start` name, stack size and priority are unknown), its entry calls `_Go()` and returns the result as the exit code, and it does not run `_Cnd_do_broadcast_at_thread_exit`, which is absent here. `_Release` wakes the launching thread with `_Cnd_broadcast`, the only waiter on the pad's condition, since libc has no `_Cnd_signal`. A failing thread start or primitive throws a host exception instead of the guest `std::system_error` of `_Throw_Cpp_error`, and a null pad or thread slot throws From 49d8cf3e61b7ebc3b78f71eec93a3b0f275318ca Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Sun, 11 Oct 2026 19:15:29 +0300 Subject: [PATCH 2/4] test(ampr): exercise simultaneous pad launches and destroyed storage --- core/libs/tests/GuestAmprThreads.cpp | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/core/libs/tests/GuestAmprThreads.cpp b/core/libs/tests/GuestAmprThreads.cpp index ea2e2e520..f87338992 100644 --- a/core/libs/tests/GuestAmprThreads.cpp +++ b/core/libs/tests/GuestAmprThreads.cpp @@ -408,6 +408,44 @@ static void TestPadLaunchMany() { } } +static unsigned APS5_VABI PadGoImmediate(TestPad* pad) { + const auto local = pad->target; + local.order->store(local.beforeRelease); + _ZNSt4_Pad8_ReleaseEv_nid_postfix(pad); + while (!local.destroyed->load()) std::this_thread::yield(); + local.order->store(local.afterRelease); + return local.exitCode; +} + +static void TestPadConcurrentLaunch() { + constexpr int Count = 4; + constexpr std::array immediateVtable{PadGoImmediate}; + std::atomic ready{0}; + std::array parents; + for (int index = 0; index < Count; ++index) { + parents[index] = std::thread([&, index] { + std::atomic order{0}; + std::atomic destroyed{false}; + TestPad pad; + ConstructPad(pad, order, destroyed, index, 0x80000000u + index); + pad.vtable = immediateVtable.data(); + ready.fetch_add(1); + while (ready.load() != Count) std::this_thread::yield(); + Pthread thread = nullptr; + _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(&pad, &thread); + Require(thread != nullptr && pad.started && order.load() == 2 * index + 1); + _ZNSt4_PadD2Ev_nid_postfix(&pad); + std::memset(&pad, 0xcd, sizeof(pad)); + destroyed = true; + int code = 0; + Require(_Thrd_join_nid_postfix(thread, &code) == ThrdSuccess); + Require(static_cast(code) == 0x80000000u + index); + Require(order.load() == 2 * index + 2); + }); + } + for (auto& parent : parents) parent.join(); +} + struct ForeignDestroy { TestPad* pad; bool rejected; @@ -447,6 +485,7 @@ static void TestPadErrors() { int main() { TestPadLaunch(); TestPadLaunchMany(); + TestPadConcurrentLaunch(); TestPadErrors(); TestThreadJoin(); TestThreadSleep(); From 8e3b2a06f24a1d6fd8cafa908317288e502564ab Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Sun, 11 Oct 2026 19:15:30 +0300 Subject: [PATCH 3/4] fix(ampr): exclude the private pad entry from the export inventory --- core/libs/prx/libSceAmpr/Export.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index e48a0dae1..a9cd3fc2b 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -273,7 +273,7 @@ static void RequireThrd(int result, const char* what) { if (result != ThrdSuccess) throw std::runtime_error(std::string(what) + " failed with " + std::to_string(result)); } -static void* APS5_VABI PadEntry(void* argument) { +static void* APS5_VABI PadEntry_nid_no_patch(void* argument) { auto* pad = static_cast(argument); const auto go = pad->vtable[0]; return reinterpret_cast(static_cast(go(pad))); @@ -935,7 +935,7 @@ int APS5_VABI _Thrd_id_nid_postfix(void) { void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, Pthread* thread) { if (!pad || !thread) APS5_INVALID_ARG_EX; - const int created = pthread_create_nid_postfix(thread, nullptr, PadEntry, pad); + const int created = pthread_create_nid_postfix(thread, nullptr, PadEntry_nid_no_patch, pad); if (created != 0) throw std::runtime_error("_Pad::_Launch: pthread_create failed with " + std::to_string(created)); while (!pad->started) RequireThrd(_Cnd_wait_nid_postfix(&pad->condition, &pad->mutex), "_Pad::_Launch: _Cnd_wait"); } From dd1deb6965d7b33f78abb8a44a2a76d88765add6 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Tue, 13 Oct 2026 15:59:12 +0300 Subject: [PATCH 4/4] fix(ampr): raise std::_Pad failures as the guest std::system_error A failing _Pad primitive threw a host std::runtime_error, which no guest catch can see. threadcall.cpp calls the checked wrappers of xthread (_Mtx_lockX, _Cnd_waitX, _Thrd_startX), which pass a non-success code to _Throw_C_error: _Thrd_nomem and _Thrd_timedout become resource_unavailable_try_again, _Thrd_busy device_or_resource_busy and _Thrd_error invalid_argument, raised through _Throw_Cpp_error, which main now implements. A failing thread start is _Thrd_startX's resource_unavailable_try_again. _Release now wakes the launching thread with _Cnd_signal, as threadcall.cpp does; libc has it since the pad was written. guest_ampr_threads checks, in a forked child whose guest terminate handler reports the thrown object, that _Release on the thread still holding the pad's mutex throws std::system_error with EBUSY. It fails with the previous host exception and with busy mapped to invalid_argument. --- core/libs/prx/libSceAmpr/Export.cpp | 26 ++++++++++++--- core/libs/tests/GuestAmprThreads.cpp | 48 +++++++++++++++++++++++++++- docs/dev/debt/libSceAmpr.md | 2 +- 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index a9cd3fc2b..f1918902d 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -225,8 +225,15 @@ static int RecordWriteAddressFromCounterPair(Apr::CommandBufferObject* buffer, v } static constexpr int ThrdSuccess = 0; +static constexpr int ThrdNomem = 1; +static constexpr int ThrdTimedout = 2; +static constexpr int ThrdBusy = 3; static constexpr int ThrdError = 4; +static constexpr int CppDeviceOrResourceBusy = 0; +static constexpr int CppInvalidArgument = 1; +static constexpr int CppResourceUnavailableTryAgain = 6; + static constexpr int MtxPlain = 0x1; static std::atomic_flag sharedPtrSpinLock; @@ -262,7 +269,7 @@ int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); 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 _Cnd_signal_nid_postfix(void** handle); 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); @@ -270,7 +277,18 @@ int SignalCondition_nid_no_patch(void** handle); } static void RequireThrd(int result, const char* what) { - if (result != ThrdSuccess) throw std::runtime_error(std::string(what) + " failed with " + std::to_string(result)); + switch (result) { + case ThrdSuccess: + return; + case ThrdNomem: + case ThrdTimedout: + ThrowCppError_nid_no_patch(CppResourceUnavailableTryAgain); + case ThrdBusy: + ThrowCppError_nid_no_patch(CppDeviceOrResourceBusy); + case ThrdError: + ThrowCppError_nid_no_patch(CppInvalidArgument); + } + throw std::runtime_error(std::string(what) + " failed with " + std::to_string(result)); } static void* APS5_VABI PadEntry_nid_no_patch(void* argument) { @@ -900,7 +918,7 @@ void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad) { if (!pad) APS5_INVALID_ARG_EX; RequireThrd(_Mtx_lock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_lock"); pad->started = true; - RequireThrd(_Cnd_broadcast_nid_postfix(&pad->condition), "_Pad::_Release: _Cnd_broadcast"); + RequireThrd(_Cnd_signal_nid_postfix(&pad->condition), "_Pad::_Release: _Cnd_signal"); RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_unlock"); } @@ -936,7 +954,7 @@ int APS5_VABI _Thrd_id_nid_postfix(void) { void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, Pthread* thread) { if (!pad || !thread) APS5_INVALID_ARG_EX; const int created = pthread_create_nid_postfix(thread, nullptr, PadEntry_nid_no_patch, pad); - if (created != 0) throw std::runtime_error("_Pad::_Launch: pthread_create failed with " + std::to_string(created)); + if (created != 0) ThrowCppError_nid_no_patch(CppResourceUnavailableTryAgain); while (!pad->started) RequireThrd(_Cnd_wait_nid_postfix(&pad->condition, &pad->mutex), "_Pad::_Launch: _Cnd_wait"); } diff --git a/core/libs/tests/GuestAmprThreads.cpp b/core/libs/tests/GuestAmprThreads.cpp index f87338992..ef98b12fe 100644 --- a/core/libs/tests/GuestAmprThreads.cpp +++ b/core/libs/tests/GuestAmprThreads.cpp @@ -10,6 +10,10 @@ #include #include #include +#ifndef _WIN32 +#include +#include +#endif struct GuestXtime { std::int64_t sec; @@ -24,6 +28,20 @@ struct PadTarget { unsigned exitCode; }; +struct GuestTypeInfo { + const void* vtable; + const char* name; +}; + +struct GuestSystemError { + const void* vtable; + const char* message; + int code; + const void* category; +}; + +using GuestTerminate = void (APS5_VABI*)(); + struct TestPad { unsigned (APS5_VABI* const* vtable)(TestPad*); void* condition; @@ -55,10 +73,14 @@ 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); +GuestTerminate APS5_VABI _ZSt13set_terminatePFvvE_nid_postfix(GuestTerminate handler); +const GuestTypeInfo* APS5_VABI __cxa_current_exception_type_nid_postfix(); +void* APS5_VABI __cxa_current_primary_exception_nid_postfix(); } static constexpr int ThrdSuccess = 0; static constexpr int ThrdError = 4; +static constexpr int GuestEbusy = 16; static constexpr int GuestEinval = 22; static constexpr int GuestErange = 34; static constexpr int GuestEilseq = 86; @@ -457,6 +479,28 @@ static void* APS5_VABI DestroyPad(void* arg) { return nullptr; } +#ifndef _WIN32 +static void APS5_VABI ExitWithSystemErrorCode() { + const auto* type = __cxa_current_exception_type_nid_postfix(); + const auto* error = static_cast(__cxa_current_primary_exception_nid_postfix()); + _exit(type && error && std::strcmp(type->name, "St12system_error") == 0 ? error->code : 1); +} + +template +static int GuestSystemErrorCode(TCall call) { + const pid_t child = fork(); + Require(child >= 0); + if (child == 0) { + _ZSt13set_terminatePFvvE_nid_postfix(ExitWithSystemErrorCode); + call(); + _exit(2); + } + int status = 0; + Require(waitpid(child, &status, 0) == child); + return WIFEXITED(status) ? WEXITSTATUS(status) : -1; +} +#endif + static void TestPadErrors() { Require(Throws([] { _ZNSt4_PadC2Ev_nid_postfix(nullptr); })); Require(Throws([] { _ZNSt4_PadD2Ev_nid_postfix(nullptr); })); @@ -470,7 +514,9 @@ static void TestPadErrors() { ConstructPad(pad, order, destroyed, 0, 0); Require(Throws([&] { _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(&pad, nullptr); })); Require(!pad.started && order.load() == 0); - Require(Throws([&] { _ZNSt4_Pad8_ReleaseEv_nid_postfix(&pad); })); +#ifndef _WIN32 + Require(GuestSystemErrorCode([&] { _ZNSt4_Pad8_ReleaseEv_nid_postfix(&pad); }) == GuestEbusy); +#endif Require(!pad.started); ForeignDestroy context{&pad, false}; diff --git a/docs/dev/debt/libSceAmpr.md b/docs/dev/debt/libSceAmpr.md index ca3383747..db01df9a8 100644 --- a/docs/dev/debt/libSceAmpr.md +++ b/docs/dev/debt/libSceAmpr.md @@ -13,4 +13,4 @@ - [sceAmprAmmGetUsageStatsData](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) fills the caller-sized 24-byte statistics structure with 512 page-table pool entries, no allocated entries, a zero high watermark and ring idle flags 0x7, the constants of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libAmpr.cpp); they are not measured on a console, and the real call is a pass-through to `sceKernelMapperGetUsageStatsData` per [drakmor/ampr_emu](https://github.com/drakmor/ampr_emu/blob/main/src/ampr_emu_amm.cpp), which this port has no kernel-side counterpart for. - [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 -- [std::_Pad](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follows Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` of Visual C++ 2013 ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. The pad is taken to be that header's class with the console's pointer handles: a vtable pointer whose first slot is the pure virtual `_Go()`, then `_Cnd_t` at 0x8, `_Mtx_t` at 0x10 and `bool _Started` at 0x18, 32 bytes, the derived `_LaunchPad` target following it; the console's `_Pad(const char*)` and `_Launch(const char*, pthread**)`/`_Launch(pthread_attr**, pthread**)` overloads suggest named primitives and could mean another layout or vtable. `_Launch` starts the thread with `pthread_create` and default attributes (the console's `_Thrd_start` name, stack size and priority are unknown), its entry calls `_Go()` and returns the result as the exit code, and it does not run `_Cnd_do_broadcast_at_thread_exit`, which is absent here. `_Release` wakes the launching thread with `_Cnd_broadcast`, the only waiter on the pad's condition, since libc has no `_Cnd_signal`. A failing thread start or primitive throws a host exception instead of the guest `std::system_error` of `_Throw_Cpp_error`, and a null pad or thread slot throws +- [std::_Pad](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follows Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` of Visual C++ 2013 ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. The pad is taken to be that header's class with the console's pointer handles: a vtable pointer whose first slot is the pure virtual `_Go()`, then `_Cnd_t` at 0x8, `_Mtx_t` at 0x10 and `bool _Started` at 0x18, 32 bytes, the derived `_LaunchPad` target following it; the console's `_Pad(const char*)` and `_Launch(const char*, pthread**)`/`_Launch(pthread_attr**, pthread**)` overloads suggest named primitives and could mean another layout or vtable. `_Launch` starts the thread with `pthread_create` and default attributes (the console's `_Thrd_start` name, stack size and priority are unknown), its entry calls `_Go()` and returns the result as the exit code, and it does not run `_Cnd_do_broadcast_at_thread_exit`, which is absent here. A failing primitive throws the guest `std::system_error` that `_Throw_C_error` picks for its code, and a failing thread start the `resource_unavailable_try_again` one of `_Thrd_startX`; any `pthread_create` error counts, since the console's `_Thrd_start` result is unknown. A null pad or thread slot throws