Skip to content
Merged
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
1 change: 1 addition & 0 deletions core/libs/prx/libc/include/Api.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,7 @@ int APS5_VABI _Mtx_init_with_name_nid_postfix(void** handle, int type, const cha
void APS5_VABI _Mtx_destroy_nid_postfix(void** handle);
int APS5_VABI _Mtx_lock_nid_postfix(void** handle);
int APS5_VABI _Mtx_trylock_nid_postfix(void** handle);
int APS5_VABI _Mtx_timedlock_nid_postfix(void** handle, const Xtime* target);
int APS5_VABI _Mtx_unlock_nid_postfix(void** handle);
int APS5_VABI _Cnd_init_nid_postfix(void** handle);
int APS5_VABI _Cnd_init_with_default_name_override_nid_postfix(void** handle, const char*);
Expand Down
148 changes: 91 additions & 57 deletions core/libs/prx/libc/src/Threads.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,27 @@ timespec XtimeDeadline(const Xtime& time, const char* function) {
return {static_cast<time_t>(seconds), static_cast<long>(nanos)};
}

std::chrono::system_clock::time_point XtimePoint(const Xtime& time, const char* function) {
const auto deadline = XtimeDeadline(time, function);
constexpr auto maxDuration = std::chrono::system_clock::duration::max();
constexpr auto minDuration = std::chrono::system_clock::duration::min();
constexpr auto maxSeconds = std::chrono::duration_cast<std::chrono::seconds>(maxDuration).count();
constexpr auto minSeconds = std::chrono::duration_cast<std::chrono::seconds>(minDuration).count();
constexpr auto maxNanos = std::chrono::duration_cast<std::chrono::nanoseconds>(maxDuration - std::chrono::seconds(maxSeconds)).count();
if (deadline.tv_sec > maxSeconds || (deadline.tv_sec == maxSeconds && deadline.tv_nsec > maxNanos) ||
deadline.tv_sec < minSeconds)
throw std::overflow_error(std::string(function) + ": xtime seconds overflow");
return std::chrono::system_clock::time_point(
std::chrono::seconds(deadline.tv_sec) +
std::chrono::ceil<std::chrono::system_clock::duration>(std::chrono::nanoseconds(deadline.tv_nsec)));
}

constexpr int threadSuccess = 0;
constexpr int threadNomem = 1;
constexpr int threadTimedout = 2;
constexpr int threadBusy = 3;
constexpr int threadError = 4;
constexpr int threadTimedlockError = 5;

constexpr int mutexRecursive = 0x100;

Expand All @@ -72,14 +88,61 @@ struct DinkumwareNativeMutex {
void lock() { EnterCriticalSection(&value); }
void unlock() { LeaveCriticalSection(&value); }
bool try_lock() { return TryEnterCriticalSection(&value) != FALSE; }

template<class Clock, class Duration>
bool try_lock_until(const std::chrono::time_point<Clock, Duration>& deadline) {
if (try_lock())
return true;
for (;;) {
const auto now = Clock::now();
if (now >= deadline)
return false;
const auto remaining = std::chrono::ceil<std::chrono::milliseconds>(deadline - now).count();
Sleep(static_cast<DWORD>(std::min<std::int64_t>(remaining, 1)));
if (try_lock()) {
if (Clock::now() <= deadline)
return true;
unlock();
return false;
}
}
}
};

struct DinkumwareNativeCondition {
CONDITION_VARIABLE value = CONDITION_VARIABLE_INIT;

void notify_one() { WakeConditionVariable(&value); }
void notify_all() { WakeAllConditionVariable(&value); }

void wait(std::unique_lock<DinkumwareNativeMutex>& lock) {
if (SleepConditionVariableCS(&value, &lock.mutex()->value, INFINITE) == FALSE)
throw std::system_error(static_cast<int>(GetLastError()), std::system_category(), "_Cnd_wait: condition wait failed");
}

template<class Clock, class Duration>
std::cv_status wait_until(std::unique_lock<DinkumwareNativeMutex>& lock,
const std::chrono::time_point<Clock, Duration>& deadline) {
for (;;) {
const auto now = Clock::now();
if (now >= deadline)
return std::cv_status::timeout;
const auto remaining = std::chrono::ceil<std::chrono::milliseconds>(deadline - now).count();
const auto largest = static_cast<std::int64_t>(std::numeric_limits<DWORD>::max() - 1U);
const auto milliseconds = static_cast<DWORD>(std::min<std::int64_t>(remaining, largest));
if (SleepConditionVariableCS(&value, &lock.mutex()->value, milliseconds) != FALSE)
return std::cv_status::no_timeout;
const DWORD error = GetLastError();
if (error != ERROR_TIMEOUT)
throw std::system_error(static_cast<int>(error), std::system_category(), "_Cnd_timedwait: condition wait failed");
if (Clock::now() >= deadline)
return std::cv_status::timeout;
}
}
};
#else
using DinkumwareNativeMutex = std::mutex;
using DinkumwareNativeCondition = std::condition_variable;
using DinkumwareNativeMutex = std::timed_mutex;
using DinkumwareNativeCondition = std::condition_variable_any;
#endif

struct DinkumwareMutex {
Expand All @@ -95,32 +158,6 @@ struct DinkumwareCondition {
DinkumwareNativeCondition native;
};

#ifdef _WIN32
DWORD WaitMillisecondsUntil(std::chrono::system_clock::time_point deadline) {
const auto now = std::chrono::system_clock::now();
if (now >= deadline)
return 0;
const auto remaining = std::chrono::ceil<std::chrono::milliseconds>(deadline - now).count();
constexpr auto largest = static_cast<std::int64_t>(std::numeric_limits<DWORD>::max() - 1U);
return static_cast<DWORD>(std::min<std::int64_t>(remaining, largest));
}

bool WaitUntil(DinkumwareCondition* condition, DinkumwareMutex* mutex, std::chrono::system_clock::time_point deadline) {
for (;;) {
const DWORD milliseconds = WaitMillisecondsUntil(deadline);
if (milliseconds == 0)
return false;
if (SleepConditionVariableCS(&condition->native.value, &mutex->native.value, milliseconds) != FALSE)
return true;
const DWORD error = GetLastError();
if (error != ERROR_TIMEOUT)
throw std::system_error(static_cast<int>(error), std::system_category(), "_Cnd_timedwait: condition wait failed");
if (std::chrono::system_clock::now() >= deadline)
return false;
}
}
#endif

DinkumwareMutex* ResolveMutex(void** handle, const char* function) {
if (!handle)
throw std::invalid_argument(std::string(function) + ": null mutex handle pointer");
Expand Down Expand Up @@ -219,6 +256,31 @@ int APS5_VABI _Mtx_trylock_nid_postfix(void** handle) {
return threadSuccess;
}

int APS5_VABI _Mtx_timedlock_nid_postfix(void** handle, const Xtime* target) {
if (!target)
APS5_INVALID_ARG_EX;
const auto deadline = XtimePoint(*target, "_Mtx_timedlock");
auto* mutex = ResolveMutex(handle, "_Mtx_timedlock");
const auto self = std::this_thread::get_id();
if (mutex->owner.load(std::memory_order_acquire) == self) {
if ((mutex->type & mutexRecursive) == 0)
return threadBusy;
if (mutex->count == std::numeric_limits<int>::max())
throw std::overflow_error("_Mtx_timedlock: recursive lock count overflow");
++mutex->count;
return threadSuccess;
}
try {
if (!mutex->native.try_lock_until(deadline))
return threadTimedout;
} catch (const std::system_error&) {
return threadTimedlockError;
}
mutex->owner.store(self, std::memory_order_release);
mutex->count = 1;
return threadSuccess;
}

int APS5_VABI _Mtx_unlock_nid_postfix(void** handle) {
auto* mutex = ResolveMutex(handle, "_Mtx_unlock");
if (!IsOwnedByCaller(mutex))
Expand Down Expand Up @@ -267,12 +329,7 @@ int APS5_VABI _Cnd_wait_nid_postfix(void** conditionHandle, void** mutexHandle)
mutex->owner.store(std::thread::id{}, std::memory_order_release);
std::unique_lock lock(mutex->native, std::adopt_lock);
try {
#ifdef _WIN32
if (SleepConditionVariableCS(&condition->native.value, &mutex->native.value, INFINITE) == FALSE)
throw std::system_error(static_cast<int>(GetLastError()), std::system_category(), "_Cnd_wait: condition wait failed");
#else
condition->native.wait(lock);
#endif
} catch (const std::system_error&) {
lock.release();
mutex->owner.store(std::this_thread::get_id(), std::memory_order_release);
Expand All @@ -288,15 +345,7 @@ int APS5_VABI _Cnd_wait_nid_postfix(void** conditionHandle, void** mutexHandle)
int APS5_VABI _Cnd_timedwait_nid_postfix(void** conditionHandle, void** mutexHandle, const Xtime* target) {
if (!target)
APS5_INVALID_ARG_EX;
const auto deadline = XtimeDeadline(*target, "_Cnd_timedwait");
constexpr auto maxDuration = std::chrono::system_clock::duration::max();
constexpr auto minDuration = std::chrono::system_clock::duration::min();
constexpr auto maxSeconds = std::chrono::duration_cast<std::chrono::seconds>(maxDuration).count();
constexpr auto minSeconds = std::chrono::duration_cast<std::chrono::seconds>(minDuration).count();
constexpr auto maxNanos = std::chrono::duration_cast<std::chrono::nanoseconds>(maxDuration - std::chrono::seconds(maxSeconds)).count();
if (deadline.tv_sec > maxSeconds || (deadline.tv_sec == maxSeconds && deadline.tv_nsec > maxNanos) ||
deadline.tv_sec < minSeconds)
throw std::overflow_error("_Cnd_timedwait: xtime seconds overflow");
const auto deadlineTime = XtimePoint(*target, "_Cnd_timedwait");
auto* condition = ResolveCondition(conditionHandle, "_Cnd_timedwait");
auto* mutex = ResolveMutex(mutexHandle, "_Cnd_timedwait");
if (!IsOwnedByCaller(mutex))
Expand All @@ -308,14 +357,7 @@ int APS5_VABI _Cnd_timedwait_nid_postfix(void** conditionHandle, void** mutexHan
std::unique_lock lock(mutex->native, std::adopt_lock);
bool timeout = false;
try {
const auto deadlineTime = std::chrono::system_clock::time_point(
std::chrono::seconds(deadline.tv_sec) +
std::chrono::ceil<std::chrono::system_clock::duration>(std::chrono::nanoseconds(deadline.tv_nsec)));
#ifdef _WIN32
timeout = !WaitUntil(condition, mutex, deadlineTime);
#else
timeout = (condition->native.wait_until(lock, deadlineTime) == std::cv_status::timeout);
#endif
} catch (const std::system_error&) {
lock.release();
mutex->owner.store(std::this_thread::get_id(), std::memory_order_release);
Expand All @@ -329,20 +371,12 @@ int APS5_VABI _Cnd_timedwait_nid_postfix(void** conditionHandle, void** mutexHan
}

int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle) {
#ifdef _WIN32
WakeAllConditionVariable(&ResolveCondition(handle, "_Cnd_broadcast")->native.value);
#else
ResolveCondition(handle, "_Cnd_broadcast")->native.notify_all();
#endif
return threadSuccess;
}

int SignalCondition_nid_no_patch(void** handle) {
#ifdef _WIN32
WakeConditionVariable(&ResolveCondition(handle, "_Cnd_signal")->native.value);
#else
ResolveCondition(handle, "_Cnd_signal")->native.notify_one();
#endif
return threadSuccess;
}

Expand Down
41 changes: 41 additions & 0 deletions core/libs/tests/GuestLibcThreads.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ static void RequireThrows(TCall call) {
}

constexpr int success = 0;
constexpr int timedout = 2;
constexpr int busy = 3;
constexpr int plain = 0x01;
constexpr int tryable = 0x02;
Expand All @@ -40,6 +41,14 @@ static void* CreateMutex(int type) {
return mutex;
}

static Xtime RelativeTime(int milliseconds) {
const auto point = std::chrono::system_clock::now() + std::chrono::milliseconds(milliseconds);
const auto since = point.time_since_epoch();
const auto seconds = std::chrono::duration_cast<std::chrono::seconds>(since);
const auto nanos = std::chrono::duration_cast<std::chrono::nanoseconds>(since - seconds);
return {seconds.count(), nanos.count()};
}

static void MutexBasics() {
void* mutex = CreateMutex(tryable);
Require(_Mtx_lock_nid_postfix(&mutex) == success);
Expand Down Expand Up @@ -110,6 +119,37 @@ static void RecursiveMutex() {

}

static void TimedMutex() {
void* mutex = CreateMutex(tryable);
Xtime future = RelativeTime(60000);
Require(_Mtx_timedlock_nid_postfix(&mutex, &future) == success);
Require(_Mtx_timedlock_nid_postfix(&mutex, &future) == busy);
Require(_Mtx_unlock_nid_postfix(&mutex) == success);

Require(_Mtx_lock_nid_postfix(&mutex) == success);
int result = -1;
std::thread worker([&] {
Xtime deadline = RelativeTime(30);
result = _Mtx_timedlock_nid_postfix(&mutex, &deadline);
});
worker.join();
Require(result == timedout);
Require(_Mtx_unlock_nid_postfix(&mutex) == success);

void* empty = nullptr;
RequireThrows<std::invalid_argument>([&] { _Mtx_timedlock_nid_postfix(&empty, &future); });
RequireThrows<std::invalid_argument>([&] { _Mtx_timedlock_nid_postfix(nullptr, &future); });
RequireThrows<std::invalid_argument>([&] { _Mtx_timedlock_nid_postfix(&mutex, nullptr); });
_Mtx_destroy_nid_postfix(&mutex);

void* recursiveMutex = CreateMutex(recursive);
Require(_Mtx_timedlock_nid_postfix(&recursiveMutex, &future) == success);
Require(_Mtx_timedlock_nid_postfix(&recursiveMutex, &future) == success);
Require(_Mtx_unlock_nid_postfix(&recursiveMutex) == success);
Require(_Mtx_unlock_nid_postfix(&recursiveMutex) == success);
_Mtx_destroy_nid_postfix(&recursiveMutex);
}

static void MutualExclusion() {
void* mutex = CreateMutex(tryable);
long counter = 0;
Expand Down Expand Up @@ -440,6 +480,7 @@ int main() {
ForwardedCalls();
MutexBasics();
RecursiveMutex();
TimedMutex();
MutualExclusion();
ConditionBroadcast();
ConditionSignal();
Expand Down
1 change: 1 addition & 0 deletions docs/dev/debt/libc.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
- [__atomic_load, __atomic_store, __atomic_exchange and __atomic_compare_exchange](../../../core/libs/prx/libc/src/AtomicBuiltins.cpp) (libc) - the generic, size-parameterised libatomic entry points are a single `ud2` in the `libc.prx` bundled with PPSA08427 (disassembly), so the console traps on any call; these throw instead
- [Sized __atomic calls](../../../core/libs/prx/libc/src/DinkumwareAtomics.cpp) (libc) - the 36 `__atomic_load_N`, `__atomic_store_N`, `__atomic_exchange_N`, `__atomic_compare_exchange_N` and `__atomic_fetch_{add,sub,and,or,xor}_N` calls (N = 1, 2, 4, 8) are sequentially consistent whatever order they are given. The `libc.prx` bundled with PPSA08427, run side by side on random values and on every order from -1 to 7, switches on the order but always uses a locked instruction or CAS loop, except that a store with an order other than `memory_order_seq_cst` is a plain store, so these are never weaker than the console. Unlike `_Atomic_load_8` and the 8-byte `_Atomic_*` exchange and fetch calls above, the console accepts every order here, including release, acq_rel and invalid ones, and these do the same
- [Dinkumware mutexes and conditions](../../../core/libs/prx/libc/src/Threads.cpp) (libc) use pointer-to-handle arguments, matching the console aerolib signatures reported in [#3079](https://github.com/boykopovar/AnyPS5/pull/3079). The title's `_Mtx_lock` maps `0` to `_Thrd_success` (0), `0x8002000b` to `_Thrd_busy` (3), and other failures to `_Thrd_error` (4); its internal mutex init recognizes recursive flag `0x100`, while every other type uses default attributes. The observed relock failure for a plain mutex suggests those defaults are error-checking, but that has not been confirmed on hardware. The title's `_Mtx_unlock` and `_Cnd_wait` return success regardless of their internal result. The host implementation treats invalid ownership preconditions as no-ops, and throws for invalid handles, host wait errors and destroying a locked mutex as debug guards; those cases are not confirmed against the console.
- [_Mtx_timedlock](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `hPzYSd5Nasc`) follows the console's `libc.prx` at `0x5d50` (PPSA02530): it takes an absolute xtime and returns `_Thrd_timedout` (2) on expiry (`0x8002003c`) or 5 on other failures. On non-Windows hosts the native mutex and condition are `std::timed_mutex` and `std::condition_variable_any`; Windows keeps the `CRITICAL_SECTION`/`CONDITION_VARIABLE` pair because `std::condition_variable_any` failed `guest_libc_cnd_timed_wait` on the Windows runner, and its `_Mtx_timedlock` polls `TryEnterCriticalSection` until the deadline. Windows timing is not measured on the console.
- [Floating-point compiler-rt builtins](../../../core/libs/prx/libc/src/FloatBuiltins.cpp) (libc) follow the `libc.prx` bundled with PPSA08427 (disassembly, and that binary run side by side on random inputs) where it differs from current compiler-rt: the soft-float `__add`, `__sub`, `__mul` and `__div` (`sf3`, `df3`) round to nearest, raise no exception flags and ignore MXCSR, here done with SSE under a default MXCSR that is restored afterwards; division flushes a quotient below the smallest normal to a signed zero; `__extendsfdf2` and `__truncdfsf2` copy NaN payloads unshifted; float-to-integer conversions do not saturate but shift the significand by a count masked to the shift width. `__mulsc3`, `__muldc3`, `__divsc3` and `__divdc3` fuse the same products as the console with `fma` and scale through the x87 like its `scalbn`, so their values match, but NaN payloads and exception flags can differ, as the host `fma` and `logb` are not the console's VEX FMA code and libm
- [random, srandom, initstate, setstate](../../../core/libs/prx/libc/src/Random.cpp) (libc) - not measured: the state `setstate` returns and its null return for a rear position past the degree, `errno` after `initstate` below 8 bytes, a null state (throws here), an `initstate` seed other than 1, concurrent callers (one lock serialises every call here) and a title's own libc. Measured in a payload process on a PS5 (firmware 12.70): a fresh process draws the `srandom(1)` sequence; `srandom` seeds an additive feedback generator through a `good_rand` that reads its argument as an int32, turns 0 into 123459876 and applies `x = 16807 * (x % 127773) - 2836 * (x / 127773)`, adding 0x7fffffff to a negative result, which reproduces seeds 0, 1, 2, 12345, 0xffffffff and 0x80000000; `initstate` returns null at 0 and 7 bytes and selects types 0 to 4 by size as measured at 8, 15, 31, 32, 63, 64, 127, 128, 129, 255, 256, 257 and 1000 bytes.
- [srandomdev](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) is not verified against the console. The firmware 3.20 [export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibcInternal.c) names NID `a2MOZf++Wjg`; it follows FreeBSD 11 [random.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdlib/random.c), filling every active generator word from host OS entropy instead of `KERN_ARND`, then resetting additive feedback positions without warming up. An entropy failure throws rather than leaving `sysctl`'s failure unchecked. Calls share the generator lock.
Expand Down
Loading