Skip to content
57 changes: 39 additions & 18 deletions core/libs/prx/libkernel/System/src/Exception.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,22 @@ bool Exited(HANDLE native) {
return WaitForSingleObject(native, 0) == WAIT_OBJECT_0;
}

bool RestoringContext(DWORD64 rip) {
static const std::array<DWORD64, 2> stubs = [] {
const HMODULE ntdll = GetModuleHandleW(L"ntdll.dll");
return std::array<DWORD64, 2>{reinterpret_cast<DWORD64>(GetProcAddress(ntdll, "NtContinue")), reinterpret_cast<DWORD64>(GetProcAddress(ntdll, "NtContinueEx"))};
}();
for (const DWORD64 stub : stubs)
if (stub != 0 && rip - stub < 0x20) return true;
return false;
}

bool InWinpthread(DWORD64 rip) {
static const HMODULE winpthread = GetModuleHandleW(L"libwinpthread-1.dll");
MEMORY_BASIC_INFORMATION info{};
return winpthread != nullptr && VirtualQuery(reinterpret_cast<const void*>(rip), &info, sizeof(info)) == sizeof(info) && info.AllocationBase == winpthread;
}

bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) {
if (thread == scePthreadSelf()) {
CONTEXT context{};
Expand All @@ -199,26 +215,31 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) {
}
const auto native = static_cast<HANDLE>(thread->nativeHandle);
auto queued = std::make_unique<Delivery>(Delivery{handler, signum, {}});
if (SuspendThread(native) == static_cast<DWORD>(-1)) {
if (Exited(native)) return false;
throw std::runtime_error("sceKernelRaiseException: cannot suspend the target thread");
}
if (Exited(native)) {
ResumeThread(native);
return false;
}
if (thread->waitCount.load(std::memory_order_seq_cst) > 0) {
const bool accepted = QueueUserAPC(WaitingEntry, native, reinterpret_cast<ULONG_PTR>(queued.get())) != 0;
ResumeThread(native);
if (!accepted) throw std::runtime_error("sceKernelRaiseException: cannot queue delivery to the waiting thread");
queued.release();
return true;
}
alignas(16) Delivery delivery{handler, signum, {}};
delivery.context.ContextFlags = CONTEXT_FULL | CONTEXT_FLOATING_POINT;
if (!GetThreadContext(native, &delivery.context)) {
for (;;) {
if (SuspendThread(native) == static_cast<DWORD>(-1)) {
if (Exited(native)) return false;
throw std::runtime_error("sceKernelRaiseException: cannot suspend the target thread");
}
if (Exited(native)) {
ResumeThread(native);
return false;
}
delivery.context.ContextFlags = CONTEXT_FULL | CONTEXT_SEGMENTS;
if (!GetThreadContext(native, &delivery.context)) {
ResumeThread(native);
throw std::runtime_error("sceKernelRaiseException: cannot read the target thread context");
}
if (thread->waitCount.load(std::memory_order_seq_cst) > 0) {
const bool accepted = QueueUserAPC(WaitingEntry, native, reinterpret_cast<ULONG_PTR>(queued.get())) != 0;
ResumeThread(native);
if (!accepted) throw std::runtime_error("sceKernelRaiseException: cannot queue delivery to the waiting thread");
queued.release();
return true;
}
if (!RestoringContext(delivery.context.Rip) && !InWinpthread(delivery.context.Rip)) break;
ResumeThread(native);
throw std::runtime_error("sceKernelRaiseException: cannot read the target thread context");
SwitchToThread();
}
const DWORD64 slot = (delivery.context.Rsp - RedZone - sizeof(Delivery)) & ~static_cast<DWORD64>(15);
if (!StackWritable(slot - HomeArea - 8, delivery.context.Rsp - RedZone)) {
Expand Down
58 changes: 58 additions & 0 deletions core/libs/tests/GuestRaiseException.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <mutex>
#include <stdexcept>
#include <thread>
#include <windows.h>

extern "C" {
int APS5_VABI sceKernelInstallExceptionHandler(int signum, void* handler);
Expand All @@ -32,6 +33,7 @@ static std::atomic<int> calls{0};
static std::atomic<std::thread::id> handlerThread;
static std::atomic<std::uint64_t> handlerRsp{0};
static std::atomic<std::uintptr_t> handlerFrame{0};
static std::atomic<Pthread> handlerSelf{nullptr};

static void APS5_VABI Handler(int signum, void* context) {
Require(signum == SIGUSR1);
Expand All @@ -41,6 +43,7 @@ static void APS5_VABI Handler(int signum, void* context) {
handlerFrame.store(reinterpret_cast<std::uintptr_t>(&local));
handlerRsp.store(rsp);
handlerThread.store(std::this_thread::get_id());
handlerSelf.store(scePthreadSelf());
calls.fetch_add(1);
}

Expand Down Expand Up @@ -102,6 +105,35 @@ static void* APS5_VABI Leaving(void* arg) {
return nullptr;
}

static constexpr DWORD ContinuedCode = 0xe0000001u;
static constexpr int ContinuingRounds = 50;
static std::atomic<int> continued{0};

static LONG CALLBACK ContinueRaised(EXCEPTION_POINTERS* info) {
if (info->ExceptionRecord->ExceptionCode != ContinuedCode) return EXCEPTION_CONTINUE_SEARCH;
continued.fetch_add(1);
return EXCEPTION_CONTINUE_EXECUTION;
}

static constexpr int SelfingRounds = 100;
static std::atomic<Pthread> selfSink{nullptr};

static void* APS5_VABI Selfing(void* arg) {
auto& worker = *static_cast<Worker*>(arg);
worker.id = std::this_thread::get_id();
worker.started.store(true);
while (!worker.stop.load()) selfSink.store(scePthreadSelf());
return nullptr;
}

static void* APS5_VABI Continuing(void* arg) {
auto& worker = *static_cast<Worker*>(arg);
worker.id = std::this_thread::get_id();
worker.started.store(true);
while (!worker.stop.load()) RaiseException(ContinuedCode, 0, 0, nullptr);
return nullptr;
}

static std::atomic<bool> finishedReturned{false};

static void* APS5_VABI Finished(void*) {
Expand Down Expand Up @@ -191,6 +223,32 @@ int main() {
Require(scePthreadJoin(leavingThread, nullptr) == 0);
Require(sceKernelDeleteSema(leaving.sem) == 0);

void* const vectored = AddVectoredExceptionHandler(1, ContinueRaised);
Require(vectored != nullptr);
Worker continuing;
Pthread continuingThread = nullptr;
Require(scePthreadCreate(&continuingThread, nullptr, Continuing, &continuing, "continuing") == 0);
while (!continuing.started.load() || continued.load() == 0) std::this_thread::yield();
for (int raised = 0; raised < ContinuingRounds; ++raised) {
Require(sceKernelRaiseException(continuingThread, SIGUSR1) == 0);
ExpectDelivery(1 + 2 * Repeats + HostRounds + LeavingRounds + raised, continuing.id);
}
continuing.stop.store(true);
Require(scePthreadJoin(continuingThread, nullptr) == 0);
Require(RemoveVectoredExceptionHandler(vectored) != 0);

Worker selfing;
Pthread selfingThread = nullptr;
Require(scePthreadCreate(&selfingThread, nullptr, Selfing, &selfing, "selfing") == 0);
while (!selfing.started.load()) std::this_thread::yield();
for (int raised = 0; raised < SelfingRounds; ++raised) {
Require(sceKernelRaiseException(selfingThread, SIGUSR1) == 0);
ExpectDelivery(1 + 2 * Repeats + HostRounds + LeavingRounds + ContinuingRounds + raised, selfing.id);
Require(handlerSelf.load() == selfingThread);
}
selfing.stop.store(true);
Require(scePthreadJoin(selfingThread, nullptr) == 0);

Pthread finishedThread = nullptr;
Require(scePthreadCreate(&finishedThread, nullptr, Finished, nullptr, "finished") == 0);
while (!finishedReturned.load()) std::this_thread::yield();
Expand Down
3 changes: 2 additions & 1 deletion docs/dev/TechnicalDebt.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,9 @@ Throughout the project, every function at every stage either **does exactly what
- [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) delivers on Windows only and throws on Linux
- [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) builds the handler's `ucontext` with FreeBSD's amd64 layout (16-byte sigmask, `mcontext` at 0x40, `mc_rsp` at 0xf8); the console's layout is not verified beyond the fields PS5Util reads
- [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) does not consult the target thread's signal mask
- [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) saves and restores only `CONTEXT_FULL | CONTEXT_FLOATING_POINT` on Windows, so the upper halves of the YMM registers are not preserved if the handler or the entry uses AVX
- [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) saves and restores only `CONTEXT_FULL | CONTEXT_SEGMENTS` on Windows, so the upper halves of the YMM registers are not preserved if the handler or the entry uses AVX
- [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) reaches a thread that waits in an alertable `TimedWait` wait through an APC. A thread blocked anywhere else, including all waits under `APS5_COARSE_TIMED_WAITS` and host waits inside the emulator, is redirected and runs the handler only when that wait returns
- [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs the handler wherever the target thread was interrupted. It waits only while the thread is inside winpthreads or restoring a context through `NtContinue`, so a handler that calls into other host code the thread was interrupted in, such as the C runtime's heap or a lock of the emulator, can deadlock on it or reenter it
- [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; its existing size formula and the 512-byte object layouts remain unverified. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`.
- [Thread cancellation](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) is not modelled: `scePthreadCancel` throws, so no cancel request is ever pending and `scePthreadTestcancel` and `pthread_testcancel` return without acting; the cancel state and type set by `scePthreadSetcancelstate` and `scePthreadSetcanceltype` are stored and not used
- [MIMG `tfe` and `lwe`](../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp) never write the status VGPR after the data. On hardware it is written only when a texel fetch hits an unmapped page of a partially resident texture, or, for `lwe` on sample/gather, when the LOD is below the T# `MIN_LOD_WARN`; texture memory is always resident here, and `lwe` on sample, gather and `image_get_lod` throws.
Expand Down
Loading