From f8c1e0c64ff857a8df03b091d778408f4fe6c8a3 Mon Sep 17 00:00:00 2001 From: popcoder565 <339285150+popcoder565@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:47:00 -0400 Subject: [PATCH 1/5] fix(libkernel): keep sceKernelRaiseException redirects out of NtContinue A raised exception redirects the target thread to a stub that runs the handler and returns through RtlRestoreContext. On Windows 10 (19045) RtlRestoreContext restores in user mode only when the context flags are exactly CONTEXT_FULL | CONTEXT_SEGMENTS (CONTEXT_XSTATE aside); for the CONTEXT_FULL | CONTEXT_FLOATING_POINT context it was given, it calls NtContinue. When the next raise suspended the thread inside that system call, NtContinue then replaced the redirected context with the one it was restoring, so the handler never ran although sceKernelRaiseException had returned 0. guest_raise_exception lost such a delivery to its busy thread in about one run in five and hung until the ctest timeout. The context is now read with CONTEXT_SEGMENTS, so the handler returns through the user-mode restore, and a raise that finds the thread inside NtContinue or NtContinueEx (an APC or an exception handler returning) resumes it and suspends it again. Co-Authored-By: Claude Opus 5.5 --- .../prx/libkernel/System/src/Exception.cpp | 51 ++++++++++++------- docs/dev/TechnicalDebt.md | 2 +- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index e0ac735c3..71bc22c90 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -190,6 +190,16 @@ bool Exited(HANDLE native) { return WaitForSingleObject(native, 0) == WAIT_OBJECT_0; } +bool RestoringContext(DWORD64 rip) { + static const std::array stubs = [] { + const HMODULE ntdll = GetModuleHandleW(L"ntdll.dll"); + return std::array{reinterpret_cast(GetProcAddress(ntdll, "NtContinue")), reinterpret_cast(GetProcAddress(ntdll, "NtContinueEx"))}; + }(); + for (const DWORD64 stub : stubs) + if (stub != 0 && rip - stub < 0x20) return true; + return false; +} + bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { if (thread == scePthreadSelf()) { CONTEXT context{}; @@ -199,26 +209,31 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { } const auto native = static_cast(thread->nativeHandle); auto queued = std::make_unique(Delivery{handler, signum, {}}); - if (SuspendThread(native) == static_cast(-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(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(-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(queued.get())) != 0; + ResumeThread(native); + if (!accepted) throw std::runtime_error("sceKernelRaiseException: cannot queue delivery to the waiting thread"); + queued.release(); + return true; + } + 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 (!RestoringContext(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(15); if (!StackWritable(slot - HomeArea - 8, delivery.context.Rsp - RedZone)) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 72e5c1cb7..849b4389c 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -220,7 +220,7 @@ 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 - [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 From c3f3cc849a8e4a50a9e5c953545d96c10184e397 Mon Sep 17 00:00:00 2001 From: popcoder565 <339285150+popcoder565@users.noreply.github.com> Date: Thu, 8 Oct 2026 00:53:58 -0400 Subject: [PATCH 2/5] test(libkernel): raise exceptions at a thread that keeps continuing SEH exceptions The new phase's vectored handler continues every exception the thread raises, so the thread spends much of its time in NtContinue, where a redirect is lost unless the raise retries. Without the retry the phase hangs in every run; with it the test passes. Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestRaiseException.cpp | 33 +++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/core/libs/tests/GuestRaiseException.cpp b/core/libs/tests/GuestRaiseException.cpp index aa715601e..8af1b6655 100644 --- a/core/libs/tests/GuestRaiseException.cpp +++ b/core/libs/tests/GuestRaiseException.cpp @@ -7,6 +7,7 @@ #include #include #include +#include extern "C" { int APS5_VABI sceKernelInstallExceptionHandler(int signum, void* handler); @@ -102,6 +103,24 @@ static void* APS5_VABI Leaving(void* arg) { return nullptr; } +static constexpr DWORD ContinuedCode = 0xe0000001u; +static constexpr int ContinuingRounds = 50; +static std::atomic 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 void* APS5_VABI Continuing(void* arg) { + auto& worker = *static_cast(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 finishedReturned{false}; static void* APS5_VABI Finished(void*) { @@ -191,6 +210,20 @@ 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); + Pthread finishedThread = nullptr; Require(scePthreadCreate(&finishedThread, nullptr, Finished, nullptr, "finished") == 0); while (!finishedReturned.load()) std::this_thread::yield(); From 175846186a0946ae99e591c930be5847faca83cd Mon Sep 17 00:00:00 2001 From: popcoder565 <339285150+popcoder565@users.noreply.github.com> Date: Thu, 8 Oct 2026 00:53:58 -0400 Subject: [PATCH 3/5] fix(libkernel): read the raise target's context before choosing APC delivery SuspendThread returns before the thread has stopped, and GetThreadContext waits until it has. waitCount was read right after SuspendThread, so a thread that was leaving its wait could still decrement it and drain its APCs with NtTestAlert before QueueUserAPC queued the delivery, which then waited for an alertable wait that never came. The context is now read first, so waitCount is read from a stopped thread. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/System/src/Exception.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index 71bc22c90..36f165762 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -219,6 +219,11 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { 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(queued.get())) != 0; ResumeThread(native); @@ -226,11 +231,6 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { queued.release(); return true; } - 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 (!RestoringContext(delivery.context.Rip)) break; ResumeThread(native); SwitchToThread(); From 384078c9246fc553619474e661457bf68713a1bf Mon Sep 17 00:00:00 2001 From: popcoder565 <339285150+popcoder565@users.noreply.github.com> Date: Thu, 8 Oct 2026 03:43:13 -0400 Subject: [PATCH 4/5] fix(libkernel): retry a raise that interrupts its target inside winpthreads MinGW's thread_local goes through emutls and winpthreads' pthread_getspecific, which holds a per-thread spinlock. A raise that redirected a thread holding it ran the handler on that same thread, and a handler reading any thread_local, scePthreadSelf for one, spun on the lock forever. A raise that finds the target inside libwinpthread-1.dll now resumes it and suspends it again, as one inside NtContinue does. guest_raise_exception's new phase raises at a thread that calls scePthreadSelf in a loop and checks that the handler sees its own thread. It hung in every run without the retry. TechnicalDebt records that a handler can still reenter other host code the thread was interrupted in. Co-Authored-By: Claude Opus 5.5 --- .../prx/libkernel/System/src/Exception.cpp | 8 +++++- core/libs/tests/GuestRaiseException.cpp | 25 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index 36f165762..6405c30a6 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -200,6 +200,12 @@ bool RestoringContext(DWORD64 rip) { 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(rip), &info, sizeof(info)) == sizeof(info) && info.AllocationBase == winpthread; +} + bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { if (thread == scePthreadSelf()) { CONTEXT context{}; @@ -231,7 +237,7 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { queued.release(); return true; } - if (!RestoringContext(delivery.context.Rip)) break; + if (!RestoringContext(delivery.context.Rip) && !InWinpthread(delivery.context.Rip)) break; ResumeThread(native); SwitchToThread(); } diff --git a/core/libs/tests/GuestRaiseException.cpp b/core/libs/tests/GuestRaiseException.cpp index 8af1b6655..e4a71a612 100644 --- a/core/libs/tests/GuestRaiseException.cpp +++ b/core/libs/tests/GuestRaiseException.cpp @@ -33,6 +33,7 @@ static std::atomic calls{0}; static std::atomic handlerThread; static std::atomic handlerRsp{0}; static std::atomic handlerFrame{0}; +static std::atomic handlerSelf{nullptr}; static void APS5_VABI Handler(int signum, void* context) { Require(signum == SIGUSR1); @@ -42,6 +43,7 @@ static void APS5_VABI Handler(int signum, void* context) { handlerFrame.store(reinterpret_cast(&local)); handlerRsp.store(rsp); handlerThread.store(std::this_thread::get_id()); + handlerSelf.store(scePthreadSelf()); calls.fetch_add(1); } @@ -113,6 +115,17 @@ static LONG CALLBACK ContinueRaised(EXCEPTION_POINTERS* info) { return EXCEPTION_CONTINUE_EXECUTION; } +static constexpr int SelfingRounds = 100; +static std::atomic selfSink{nullptr}; + +static void* APS5_VABI Selfing(void* arg) { + auto& worker = *static_cast(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(arg); worker.id = std::this_thread::get_id(); @@ -224,6 +237,18 @@ int main() { 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(); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 849b4389c..b163ec184 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -222,6 +222,7 @@ Throughout the project, every function at every stage either **does exactly what - [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_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. From 24dc98d7c035bde08e679e3c62a59402a2b57cc5 Mon Sep 17 00:00:00 2001 From: popcoder565 <339285150+popcoder565@users.noreply.github.com> Date: Thu, 8 Oct 2026 09:47:25 -0400 Subject: [PATCH 5/5] fix(libkernel): give up a raise whose target stays in NtContinue or winpthreads The retry that waits for a target to leave NtContinue, NtContinueEx or libwinpthread-1.dll had no bound, so a target that stayed there made sceKernelRaiseException spin forever. It now throws after 1 s. A new guest_raise_exception phase keeps a thread inside NtContinue by continuing a context that calls NtContinue again, and checks that the raise throws and that the handler did not run. Co-Authored-By: Claude Opus 5.5 --- .../prx/libkernel/System/src/Exception.cpp | 4 ++ core/libs/tests/GuestRaiseException.cpp | 49 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index 6405c30a6..b40e31546 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -5,6 +5,7 @@ #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" #include +#include #include #include #include @@ -101,6 +102,7 @@ GuestExceptionHandler Handler(int signum) { #ifdef _WIN32 constexpr std::size_t RedZone = 128; constexpr std::size_t HomeArea = 32; +constexpr auto RetryLimit = std::chrono::seconds(1); struct Delivery { GuestExceptionHandler handler; @@ -216,6 +218,7 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { const auto native = static_cast(thread->nativeHandle); auto queued = std::make_unique(Delivery{handler, signum, {}}); alignas(16) Delivery delivery{handler, signum, {}}; + const auto retryDeadline = std::chrono::steady_clock::now() + RetryLimit; for (;;) { if (SuspendThread(native) == static_cast(-1)) { if (Exited(native)) return false; @@ -239,6 +242,7 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { } if (!RestoringContext(delivery.context.Rip) && !InWinpthread(delivery.context.Rip)) break; ResumeThread(native); + if (std::chrono::steady_clock::now() >= retryDeadline) throw std::runtime_error("sceKernelRaiseException: the target thread stayed inside NtContinue or winpthreads for 1 s"); SwitchToThread(); } const DWORD64 slot = (delivery.context.Rsp - RedZone - sizeof(Delivery)) & ~static_cast(15); diff --git a/core/libs/tests/GuestRaiseException.cpp b/core/libs/tests/GuestRaiseException.cpp index e4a71a612..ceed4647a 100644 --- a/core/libs/tests/GuestRaiseException.cpp +++ b/core/libs/tests/GuestRaiseException.cpp @@ -134,6 +134,28 @@ static void* APS5_VABI Continuing(void* arg) { return nullptr; } +using NtContinueFunction = LONG(NTAPI*)(CONTEXT*, BOOLEAN); +alignas(16) static CONTEXT loopContext; +alignas(16) static CONTEXT leaveContext; +static std::atomic loopingId{0}; + +static void* APS5_VABI Looping(void* arg) { + auto& worker = *static_cast(arg); + const auto ntContinue = reinterpret_cast(reinterpret_cast(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtContinue"))); + volatile bool left = false; + RtlCaptureContext(&leaveContext); + if (left) return nullptr; + left = true; + loopContext = leaveContext; + loopContext.Rip = reinterpret_cast(ntContinue); + loopContext.Rcx = reinterpret_cast(&loopContext); + loopContext.Rdx = 0; + loopingId.store(GetCurrentThreadId()); + worker.started.store(true); + ntContinue(&loopContext, FALSE); + return nullptr; +} + static std::atomic finishedReturned{false}; static void* APS5_VABI Finished(void*) { @@ -249,6 +271,33 @@ int main() { selfing.stop.store(true); Require(scePthreadJoin(selfingThread, nullptr) == 0); + Worker looping; + Pthread loopingThread = nullptr; + Require(scePthreadCreate(&loopingThread, nullptr, Looping, &looping, "looping") == 0); + while (!looping.started.load()) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + const int beforeLooping = calls.load(); + const auto raisedAt = std::chrono::steady_clock::now(); + bool refused = false; + try { + sceKernelRaiseException(loopingThread, SIGUSR1); + } catch (const std::runtime_error&) { + refused = true; + } + const auto retried = std::chrono::steady_clock::now() - raisedAt; + Require(refused && retried >= std::chrono::milliseconds(900) && retried < std::chrono::seconds(10)); + Require(calls.load() == beforeLooping); + const HANDLE loopingHandle = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT, FALSE, loopingId.load()); + Require(loopingHandle != nullptr); + Require(SuspendThread(loopingHandle) != static_cast(-1)); + CONTEXT stopped{}; + stopped.ContextFlags = CONTEXT_CONTROL; + Require(GetThreadContext(loopingHandle, &stopped) != 0); + loopContext = leaveContext; + Require(ResumeThread(loopingHandle) != static_cast(-1)); + CloseHandle(loopingHandle); + Require(scePthreadJoin(loopingThread, nullptr) == 0); + Pthread finishedThread = nullptr; Require(scePthreadCreate(&finishedThread, nullptr, Finished, nullptr, "finished") == 0); while (!finishedReturned.load()) std::this_thread::yield(); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b163ec184..05c9990bb 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -222,7 +222,7 @@ Throughout the project, every function at every stage either **does exactly what - [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_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 +- [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`, and throws if that lasts 1 s, 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.