Merge pull request #3743 from YossiYad/carry/ampr-pad-launch

feat(libSceAmpr): implement the std::_Pad thread launch helpers
This commit is contained in:
Adrià 2026-10-13 15:15:37 +02:00 committed by GitHub
commit 1e7f4967ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 297 additions and 12 deletions

View File

@ -12,6 +12,7 @@
#include <cstring>
#include <limits>
#include <stdexcept>
#include <string>
#include <thread>
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<GuestPad*>(argument);
const auto go = pad->vtable[0];
return reinterpret_cast<void*>(static_cast<std::uintptr_t>(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) {

View File

@ -10,13 +10,51 @@
#include <cstring>
#include <stdexcept>
#include <thread>
#ifndef _WIN32
#include <sys/wait.h>
#include <unistd.h>
#endif
struct GuestXtime {
std::int64_t sec;
std::int64_t nsec;
};
struct PadTarget {
std::atomic<int>* order;
std::atomic<bool>* 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<unsigned (APS5_VABI*)(TestPad*), 1> PadVtable{PadGo};
template <typename TException, typename TCall>
static bool Throws(TCall call) {
try {
call();
} catch (const TException&) {
return true;
}
return false;
}
static void ConstructPad(TestPad& pad, std::atomic<int>& order, std::atomic<bool>& 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<int> order{0};
std::atomic<bool> 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<std::atomic<int>, Count> orders{};
std::atomic<bool> destroyed{false};
std::array<TestPad, Count> pads{};
std::array<Pthread, Count> 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<unsigned (APS5_VABI*)(TestPad*), 1> immediateVtable{PadGoImmediate};
std::atomic<int> ready{0};
std::array<std::thread, Count> parents;
for (int index = 0; index < Count; ++index) {
parents[index] = std::thread([&, index] {
std::atomic<int> order{0};
std::atomic<bool> 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<unsigned>(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<ForeignDestroy*>(arg);
context.rejected = Throws<std::logic_error>([&] { _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<const GuestSystemError*>(__cxa_current_primary_exception_nid_postfix());
_exit(type && error && std::strcmp(type->name, "St12system_error") == 0 ? error->code : 1);
}
template <typename TCall>
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<std::invalid_argument>([] { _ZNSt4_PadC2Ev_nid_postfix(nullptr); }));
Require(Throws<std::invalid_argument>([] { _ZNSt4_PadD2Ev_nid_postfix(nullptr); }));
Require(Throws<std::invalid_argument>([] { _ZNSt4_Pad8_ReleaseEv_nid_postfix(nullptr); }));
Pthread thread = nullptr;
Require(Throws<std::invalid_argument>([&] { _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(nullptr, &thread); }));
std::atomic<int> order{0};
std::atomic<bool> destroyed{true};
TestPad pad;
ConstructPad(pad, order, destroyed, 0, 0);
Require(Throws<std::invalid_argument>([&] { _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<std::invalid_argument>([&] { _ZNSt4_PadD2Ev_nid_postfix(&pad); }));
}
int main() {
TestPadLaunch();
TestPadLaunchMany();
TestPadConcurrentLaunch();
TestPadErrors();
TestThreadJoin();
TestThreadSleep();
TestSpinLock();

View File

@ -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