diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index f5e5f01a6..f1918902d 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; @@ -224,8 +225,17 @@ 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; struct AmmUsageStatsData { @@ -240,14 +250,53 @@ 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_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); [[noreturn]] void ThrowCppError_nid_no_patch(int code); } +static void RequireThrd(int result, const char* what) { + 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) { + 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 +914,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_signal_nid_postfix(&pad->condition), "_Pad::_Release: _Cnd_signal"); + 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 +951,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_nid_no_patch, pad); + if (created != 0) ThrowCppError_nid_no_patch(CppResourceUnavailableTryAgain); + 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..ef98b12fe 100644 --- a/core/libs/tests/GuestAmprThreads.cpp +++ b/core/libs/tests/GuestAmprThreads.cpp @@ -10,13 +10,51 @@ #include #include #include +#ifndef _WIN32 +#include +#include +#endif struct GuestXtime { std::int64_t sec; std::int64_t nsec; }; +struct PadTarget { + std::atomic* order; + std::atomic* destroyed; + int beforeRelease; + int afterRelease; + 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; + 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); @@ -35,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; @@ -316,7 +358,181 @@ 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); + } +} + +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; +}; + +static void* APS5_VABI DestroyPad(void* arg) { + auto& context = *static_cast(arg); + context.rejected = Throws([&] { _ZNSt4_PadD2Ev_nid_postfix(context.pad); }); + 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); })); + 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); +#ifndef _WIN32 + Require(GuestSystemErrorCode([&] { _ZNSt4_Pad8_ReleaseEv_nid_postfix(&pad); }) == GuestEbusy); +#endif + 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(); + TestPadConcurrentLaunch(); + TestPadErrors(); TestThreadJoin(); TestThreadSleep(); TestSpinLock(); diff --git a/docs/dev/debt/libSceAmpr.md b/docs/dev/debt/libSceAmpr.md index 3b088674e..db01df9a8 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. 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