Merge pull request #12135 from Oscar45397/fix/libkernel-raise-short-sleep

fix(libkernel): deliver raises at the spin that ends a sleep on Windows
This commit is contained in:
Adrià authored and GitHub committed 2026-10-30 17:05:05 +01:00
commit 9304896d89
4 files changed
+95 -9

No files matched your search

+10
View File
@@ -2276,6 +2276,16 @@ if(BUILD_TESTING)
add_test(NAME guest_raise_leaves_host_code COMMAND guest_raise_leaves_host_code_tests)
set_tests_properties(guest_raise_leaves_host_code PROPERTIES TIMEOUT 60 ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
add_test_executable(guest_raise_short_sleep_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseShortSleep.cpp)
target_include_directories(guest_raise_short_sleep_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_raise_short_sleep_tests PRIVATE libkernel libc)
configure_windows_unwind(guest_raise_short_sleep_tests)
add_test(NAME guest_raise_short_sleep COMMAND guest_raise_short_sleep_tests)
set_tests_properties(guest_raise_short_sleep PROPERTIES TIMEOUT 30 ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
add_test(NAME guest_raise_short_sleep_without_wake COMMAND guest_raise_short_sleep_tests)
set_tests_properties(guest_raise_short_sleep_without_wake PROPERTIES TIMEOUT 30 ENVIRONMENT "APS5_NO_SLEEP_WAKE=1" ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
add_test_executable(guest_mutex_contention_tests ${LIBS_INCLUDE_DIR}/tests/GuestMutexContention.cpp)
target_include_directories(guest_mutex_contention_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_mutex_contention_tests PRIVATE libkernel libc)
+20 -8
View File
@@ -530,10 +530,14 @@ void SleepUntil(std::uint64_t deadlineNanos) {
if (!TimerWait(timer, deadlineNanos - now - LEAD_NANOS)) AlertableSleep(WholeMilliseconds(deadlineNanos - now - LEAD_NANOS));
now = NowNanos();
}
while (now < deadlineNanos) {
YieldProcessor();
now = NowNanos();
{
DeliveryDeferral deferral;
while (now < deadlineNanos) {
YieldProcessor();
now = NowNanos();
}
}
DrainApcs();
#else
const std::uint64_t now = NowNanos();
if (deadlineNanos > now) SleepNanos(deadlineNanos - now);
@@ -580,12 +584,20 @@ bool SleepNanosUnlessChanged(std::uint64_t nanos, const std::atomic<std::uint64_
if (counter.load(std::memory_order_seq_cst) != observed) return true;
now = NowNanos();
}
while (now < deadlineNanos) {
if (counter.load(std::memory_order_seq_cst) != observed) return true;
YieldProcessor();
now = NowNanos();
bool changed = false;
{
DeliveryDeferral deferral;
while (now < deadlineNanos) {
if (counter.load(std::memory_order_seq_cst) != observed) {
changed = true;
break;
}
YieldProcessor();
now = NowNanos();
}
}
return false;
DrainApcs();
return changed;
#else
std::unique_lock lock(ChangeSleepersMutex());
const auto deadline = std::chrono::steady_clock::now() + std::chrono::nanoseconds(nanos);
+64
View File
@@ -0,0 +1,64 @@
#include "prx/libkernel/include/Api.hpp"
#include "prx/libkernel/Time/include/Time.hpp"
#include "SceTypes.hpp"
#include <atomic>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <thread>
static constexpr int SigUsr1 = 30;
static constexpr int Raises = 100;
static void Require(bool value) { if (!value) std::abort(); }
static std::atomic<int> deliveries{0};
static std::atomic<bool> stop{false};
static std::atomic<unsigned long long> naps{0};
static std::atomic<KernelUseconds> napMicros{0};
static void APS5_VABI Handler(int, void*) { deliveries.fetch_add(1); }
static void* APS5_VABI Napper(void*) {
while (!stop.load()) {
sceKernelUsleep_nid_postfix(napMicros.load());
naps.fetch_add(1);
}
return nullptr;
}
static void RaiseAtNapper(KernelUseconds micros) {
napMicros.store(micros);
stop.store(false);
naps.store(0);
Pthread napper = nullptr;
Require(scePthreadCreate(&napper, nullptr, Napper, nullptr, "napper") == 0);
while (naps.load() == 0) std::this_thread::yield();
for (int raise = 0; raise < Raises; ++raise) {
const int before = deliveries.load();
Require(sceKernelRaiseException(napper, SigUsr1) == 0);
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
while (deliveries.load() == before) {
if (std::chrono::steady_clock::now() >= deadline) {
std::fprintf(stderr, "raise %d at a thread sleeping %u us in a loop was not delivered within 3 s (%llu sleeps since)\n", raise, micros, naps.load());
std::abort();
}
std::this_thread::yield();
}
}
stop.store(true);
Require(scePthreadJoin(napper, nullptr) == 0);
}
int main() {
std::atomic<bool> done{false};
std::thread watchdog([&done] {
for (int i = 0; i < 20000 && !done.load(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1));
if (!done.load()) std::abort();
});
Require(sceKernelInstallExceptionHandler(SigUsr1, reinterpret_cast<void*>(&Handler)) == 0);
for (const KernelUseconds micros : {100u, 200u, 400u}) RaiseAtNapper(micros);
Require(sceKernelRemoveExceptionHandler(SigUsr1) == 0);
std::printf("%d raises delivered\n", deliveries.load());
done.store(true);
watchdog.join();
}
+1 -1
View File
@@ -214,7 +214,7 @@
- On macOS the guest runs on a second thread and the main thread serves the main dispatch queue from its run loop: AppKit (SDL's Cocoa windows, Metal surfaces, event pumping and game controllers) only works on the main thread, and a guest never hands its thread back. The macOS entry stub of the relinker starts the guest through `Aps5StartGuest` ([MainThreadMacOs.cpp](../../../core/libs/prx/libkernel/Pthread/src/MainThreadMacOs.cpp)), on a thread with the stack size of the main thread (at least 8 MiB), and the guest ends the process with `exit`; a guest entry that returns aborts. libSceVideoOut sends its SDL calls to the main thread with `Aps5RunOnMainThread` (`OnMainThread` in [MainThread.hpp](../../../core/libs/prx/libSceVideoOut/include/MainThread.hpp)), which waits for them and rethrows their exceptions; a host program that never called `Aps5StartGuest` runs them in place
- On macOS the Rosetta main stack and the stacks of the host threads lie inside the guest arena, where a title can map fixed addresses. `Aps5StartGuest` moves the main thread onto a 16 MiB stack at `0x600000000000`, copies `argv`, `environ`, the program name and the initial block off the old stack, and releases the old stack range to the arena ([MainThreadMacOs.cpp](../../../core/libs/prx/libkernel/Pthread/src/MainThreadMacOs.cpp)). The guest thread and the other host threads still take their stacks from the system, so a title that fixes a mapping over one of those stacks fails with a host region overlap. The move is skipped when the main stack lies outside the arena, when the new stack cannot be placed outside it, and on hosts that are not x86-64
- [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 a queued delivery (an APC) in the first alertable wait of libkernel's TimedWait the target enters, or where libkernel drains APCs after releasing its address-wait or park queue lock (both are taken under a delivery deferral). Host code that holds a lock of its own across such a wait keeps it held while a waiting handler runs
- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs a queued delivery (an APC) in the first alertable wait of libkernel's TimedWait the target enters, or where libkernel drains APCs after releasing its address-wait or park queue lock or after the spin that ends a sleep (each is taken under a delivery deferral). Host code that holds a lock of its own across such a wait keeps it held while a waiting handler runs
- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) leaves the signal pending on a target thread that was created suspended and is not resumed yet, and delivers it on that thread once it is resumed, before its start routine; further raises while it is pending are merged into that one delivery. FreeBSD 11.4 libthr starts such a thread with every signal but `SIGTRAP` blocked ([`thr_create.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_create.c#L177-L183)) and restores its mask only once it is resumed ([`thread_start`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_create.c#L252-L294)), so a signal sent to it stays pending until then. The console's behaviour is unverified
- [sigaltstack](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records each thread's alternate signal stack and reports it back, following FreeBSD's `kern_sigaltstack` (`EINVAL` for flags other than `SS_DISABLE`, `ENOMEM` below `MINSIGSTKSZ` 2048), but handlers never run on it, so `SS_ONSTACK` and `EPERM` are never reported
- [sigpending](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) reports the intersection of the process-wide pending set of `sigtimedwait` and the blocked mask of `_sigprocmask`, as FreeBSD's `sys_sigpending` intersects the pending list with `td_sigmask`, and returns -1 with `EFAULT` for a null set. PPSA99420 imports it