Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 62 additions & 12 deletions core/libs/prx/libSceAmpr/Export.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <cstring>
#include <limits>
#include <stdexcept>
#include <string>
#include <thread>

static constexpr int SCE_AMPR_ERROR_BUFFER_FULL = 0x8002001C;
Expand Down Expand Up @@ -229,10 +230,30 @@ static constexpr int GuestClockRealtime = 0;
static constexpr std::int64_t NanosPerSecond = 1000000000;
static constexpr std::int64_t LongestSleepSeconds = 86400;

static constexpr int MtxPlain = 0x1;

static std::atomic_flag sharedPtrSpinLock;

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);
}

Expand All @@ -252,6 +273,16 @@ static KernelTimespec XtimeDeadline(const Xtime& time) {
return {seconds, nanos};
}

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<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) {
Expand Down Expand Up @@ -865,29 +896,48 @@ 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) {
NotImplemented_nid_no_patch(__func__);
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) {
Expand Down
132 changes: 132 additions & 0 deletions core/libs/tests/GuestAmprThreads.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <array>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstdio>
Expand All @@ -15,7 +16,27 @@ struct GuestXtime {
std::int64_t nsec;
};

struct PadTarget {
std::atomic<int>* order;
std::atomic<bool>* 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);
Expand Down Expand Up @@ -197,7 +218,118 @@ static void TestWcstombs() {
Require(result == Failed);
}

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);
}
}

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;
}

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);
Require(Throws<std::runtime_error>([&] { _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<std::invalid_argument>([&] { _ZNSt4_PadD2Ev_nid_postfix(&pad); }));
}

int main() {
TestPadLaunch();
TestPadLaunchMany();
TestPadErrors();
TestThreadJoin();
TestThreadSleep();
TestSpinLock();
Expand Down
1 change: 1 addition & 0 deletions docs/dev/TechnicalDebt.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ Throughout the project, every function at every stage either **does exactly what
- [mkstemp and isatty](../../core/libs/prx/libScePosixForWebKit/Filesystem.cpp) (libScePosixForWebKit) follow FreeBSD's [temporary-file API](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/stdio/mktemp.c) and [terminal check](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/gen/isatty.c), without console verification. `mkstemp` replaces all trailing `X` characters, accepts a fixed name, and creates files through the guest path resolver with exclusive read/write access and mode 0600. Windows file permissions follow the existing kernel file implementation. `isatty` tests Windows console handles rather than all character devices; files, pipes and guest sockets return `ENOTTY`, closed descriptors return `EBADF`.
- [arc4random](../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) returns four bytes from the same thread-local host `std::random_device` as `arc4random_buf`, uniform over the 32-bit range like FreeBSD 11.4's RC4 keystream in [arc4random.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/arc4random.c). The console's generator and its reseeding are unknown; `arc4random_uniform`, `arc4random_stir` and `arc4random_addrandom` are not in the module's export set and are absent
- [Dinkumware thread exports](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`, `xtime.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. `_Thrd_t` is taken to be the guest `pthread_t`, as the `std::_Pad::_Launch(pthread**)` export suggests: `_Thrd_join` returns `_Thrd_error` (4) when `pthread_join` fails and stores the thread's return value truncated to `int`. `_Thrd_sleep` reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep`, so it is a cancellation point; a null xtime or one whose normalization overflows throws. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. `_Thrd_join` (32-bit store of the result, 0 or 4) and the spin lock (byte exchange loop, no yield) match that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning
- [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
- [libSceZlib](../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports one outstanding request, null-timeout waits and one result retrieval. Timed waits, multiple outstanding requests, repeated retrieval and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI.
- [Windows file timestamps](../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes` and `futimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond
- [fchdir](../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) changes the guest working directory to the descriptor's directory: on Windows through the path recorded when the descriptor was opened, on Linux through the `/proc/self/fd` link target. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. FreeBSD `change_dir`'s search-permission check (`EACCES`) is not modelled, and a directory renamed after it was opened is still reached through the recorded path. Console behaviour is unverified
Expand Down
Loading