From a58084520330133ca12ed4ce31932b6ec5e50e26 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:52:34 -0700 Subject: [PATCH 1/6] fix(libkernel): hand signalled semaphore tokens to queued waiters sceKernelSignalSema only added to the count and woke the waiters, each of which then had to take the count itself. A sceKernelPollSema or sceKernelWaitSema that ran first took the tokens instead, so a thread that had queued for them could time out or keep waiting. #4899 fixes the same steal for event flags. sceKernelSignalSema now hands its tokens to the queued waiters in the order they queued, passing over one that needs more than is left, and only what remains stays on the count. sceKernelCancelSema reports the queued waiters and cancels them. shadPS4's semaphore serves its wait list the same way when it is signalled. --- core/libs/prx/libkernel/CMakeLists.txt | 10 ++++ .../libkernel/Semaphore/include/Semaphore.hpp | 8 ++- .../prx/libkernel/Semaphore/src/Semaphore.cpp | 58 ++++++++++--------- core/libs/tests/GuestSemaphoreHandoff.cpp | 54 +++++++++++++++++ 4 files changed, 103 insertions(+), 27 deletions(-) create mode 100644 core/libs/tests/GuestSemaphoreHandoff.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 72ce92342a..5734a5de07 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -554,6 +554,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_cancel_recovery guest_pthread_cancel_recovery_coarse PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_semaphore_handoff_tests ${LIBS_INCLUDE_DIR}/tests/GuestSemaphoreHandoff.cpp) + target_include_directories(guest_semaphore_handoff_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_semaphore_handoff_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_semaphore_handoff_tests) + add_test(NAME guest_semaphore_handoff COMMAND guest_semaphore_handoff_tests) + set_tests_properties(guest_semaphore_handoff PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_semaphore_handoff PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_semaphore_tests ${LIBS_INCLUDE_DIR}/tests/GuestSemaphore.cpp) target_include_directories(guest_semaphore_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_semaphore_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Semaphore/include/Semaphore.hpp b/core/libs/prx/libkernel/Semaphore/include/Semaphore.hpp index 08c0505979..3157e23a61 100644 --- a/core/libs/prx/libkernel/Semaphore/include/Semaphore.hpp +++ b/core/libs/prx/libkernel/Semaphore/include/Semaphore.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -13,6 +14,11 @@ constexpr int KERNEL_SEMA_OK = 0; +struct KernelSemaWaiter { + std::int32_t need; + bool released = false; +}; + struct KernelSemaPrivate { KernelSemaPrivate(std::int32_t initCount, std::int32_t maxCount, std::string name, bool isFifo); @@ -25,7 +31,7 @@ struct KernelSemaPrivate { bool isFifo; bool deleted = false; std::int32_t waiterCount = 0; - std::int32_t cancelableCount = 0; + std::list queue; std::uint64_t cancelGeneration = 0; }; diff --git a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp index 7375ae7ff2..0126d8fc76 100644 --- a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp +++ b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp @@ -45,6 +45,16 @@ int APS5_VABI sceKernelSignalSema(KernelSema sem, int count) { return SCE_KERNEL_ERROR_EINVAL; } sem->tokenCount += count; + for (auto it = sem->queue.begin(); it != sem->queue.end();) { + KernelSemaWaiter* waiter = *it; + if (waiter->need > sem->tokenCount) { + ++it; + continue; + } + sem->tokenCount -= waiter->need; + waiter->released = true; + it = sem->queue.erase(it); + } sem->condition.NotifyAll(); return KERNEL_SEMA_OK; } @@ -55,54 +65,50 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time) } std::unique_lock lock(sem->mutex); + if (sem->tokenCount >= need) { + sem->tokenCount -= need; + return KERNEL_SEMA_OK; + } const std::uint64_t generation = sem->cancelGeneration; + KernelSemaWaiter waiter{need}; + sem->queue.push_back(&waiter); ++sem->waiterCount; - ++sem->cancelableCount; struct WaiterGuard { KernelSemaPrivate* sem; - std::uint64_t generation; + KernelSemaWaiter* waiter; ~WaiterGuard() { --sem->waiterCount; - if (sem->cancelGeneration == generation) { - --sem->cancelableCount; + if (!waiter->released) { + sem->queue.remove(waiter); } sem->condition.NotifyAll(); } - } waiterGuard{sem, generation}; + } waiterGuard{sem, &waiter}; const auto waitStart = std::chrono::steady_clock::now(); const auto traceWait = [&](bool timedOut) { KernelTraceWait_nid_postfix("sema", __builtin_return_address(0), static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - waitStart).count()), timedOut); }; - const auto released = [&] { return sem->tokenCount >= need || sem->deleted || sem->cancelGeneration != generation; }; + const auto released = [&] { return waiter.released || sem->deleted || sem->cancelGeneration != generation; }; + bool acquired = true; if (time == nullptr) { sem->condition.Wait(lock, released); - traceWait(false); - if (sem->deleted) { - return SCE_KERNEL_ERROR_EACCES; - } - if (sem->cancelGeneration != generation) { - return SCE_KERNEL_ERROR_ECANCELED; - } - sem->tokenCount -= need; + } else { + const std::uint64_t deadline = TimedWait::DeadlineNanos(*time); + acquired = sem->condition.WaitUntil(lock, deadline, released); + *time = static_cast(TimedWait::RemainingMicros(deadline)); + } + traceWait(!acquired); + if (waiter.released) { return KERNEL_SEMA_OK; } - - const std::uint64_t deadline = TimedWait::DeadlineNanos(*time); - const bool acquired = sem->condition.WaitUntil(lock, deadline, released); - *time = static_cast(TimedWait::RemainingMicros(deadline)); - traceWait(!acquired); if (sem->deleted) { return SCE_KERNEL_ERROR_EACCES; } if (sem->cancelGeneration != generation) { return SCE_KERNEL_ERROR_ECANCELED; } - if (!acquired) { - return SCE_KERNEL_ERROR_ETIMEDOUT; - } - sem->tokenCount -= need; - return KERNEL_SEMA_OK; + return SCE_KERNEL_ERROR_ETIMEDOUT; } int APS5_VABI sceKernelCancelSema(KernelSema sem, int count, int* threads) { @@ -115,10 +121,10 @@ int APS5_VABI sceKernelCancelSema(KernelSema sem, int count, int* threads) { return SCE_KERNEL_ERROR_EINVAL; } if (threads != nullptr) { - *threads = sem->cancelableCount; + *threads = static_cast(sem->queue.size()); } sem->tokenCount = count < 0 ? sem->initCount : count; - sem->cancelableCount = 0; + sem->queue.clear(); ++sem->cancelGeneration; sem->condition.NotifyAll(); return KERNEL_SEMA_OK; diff --git a/core/libs/tests/GuestSemaphoreHandoff.cpp b/core/libs/tests/GuestSemaphoreHandoff.cpp new file mode 100644 index 0000000000..c879b6feb8 --- /dev/null +++ b/core/libs/tests/GuestSemaphoreHandoff.cpp @@ -0,0 +1,54 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI sceKernelCreateSema(KernelSema* sem, const char* name, uint32_t attr, int init, int max, void* opt); +int APS5_VABI sceKernelDeleteSema(KernelSema sem); +int APS5_VABI sceKernelPollSema(KernelSema sem, int need); +int APS5_VABI sceKernelSignalSema(KernelSema sem, int count); +int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time); +} + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); + +static void Require(bool value) { if (!value) std::abort(); } + +struct Waiter { + KernelSema sem = nullptr; + int result = -1; + std::atomic started{false}; +}; + +static void* APS5_VABI Wait(void* arg) { + auto& waiter = *static_cast(arg); + KernelUseconds timeout = 1000000; + waiter.started = true; + waiter.result = sceKernelWaitSema(waiter.sem, 1, &timeout); + return nullptr; +} + +int main() { + for (int i = 0; i < 20; ++i) { + KernelSema sem = nullptr; + Require(sceKernelCreateSema(&sem, "handoff", 1, 0, 1, nullptr) == SCE_OK); + Waiter waiter{sem}; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Wait, &waiter, nullptr) == SCE_OK); + while (!waiter.started) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(sceKernelSignalSema(sem, 1) == SCE_OK); + Require(sceKernelPollSema(sem, 1) == SCE_KERNEL_ERROR_EBUSY); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + Require(waiter.result == SCE_OK); + Require(sceKernelPollSema(sem, 1) == SCE_KERNEL_ERROR_EBUSY); + Require(sceKernelDeleteSema(sem) == SCE_OK); + } + return 0; +} From 67f2f4df8d2aef1226bd223ef6e4d62d27cb3fa9 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:03:03 -0700 Subject: [PATCH 2/6] docs(libkernel): record the order semaphore waiters are served in sceKernelSignalSema serves queued waiters in arrival order and passes over one whose need is larger than the tokens left, as shadPS4 does; the priority order SCE_KERNEL_SEMA_ATTR_TH_PRIO asks for is not modelled, and the console's order is unverified. --- docs/dev/debt/libkernel.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 637ab5eb36..f5e3f73b5d 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -97,6 +97,7 @@ - [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters; other filters, `EV_DISABLE`, `EV_DISPATCH`, `EV_FORCEONESHOT`, `NOTE_LOWAT`, `EV_CLEAR` on a socket, `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro), any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) and a negative `nevents` throw. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop - [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title - [sem_timedwait](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. +- [sceKernelWaitSema and sceKernelSignalSema](../../../core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp) (libkernel) queue waiters in arrival order. `sceKernelSignalSema` hands its tokens to them in that order, passing over one that needs more than is left, and a `sceKernelWaitSema` or `sceKernelPollSema` that finds enough tokens takes them without queueing, as shadPS4's [semaphore](https://github.com/shadps4-emu/shadPS4/blob/06e813ff8c8e2a6d81b02cabfcc9543c3debf78e/src/core/libraries/kernel/threads/semaphore.cpp#L159-L206) does. `SCE_KERNEL_SEMA_ATTR_TH_PRIO` (0x02), for which shadPS4 orders the queue by thread priority, is not modelled, and the console's order is unverified - [tcsendbreak, tcdrain, tcflush, tcflow, tcgetpgrp, tcsetpgrp](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) follow FreeBSD 11.4's [`termios.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/termios.c): `tcflush` answers `EINVAL` for a queue other than `TCIFLUSH`, `TCOFLUSH` or `TCIOFLUSH` and `tcflow` for an action outside `TCOOFF`..`TCION` before the descriptor is looked at, and every other call fails as its terminal ioctl does on a descriptor that is not a terminal, with the errors `tcgetattr` gives, so `tcsendbreak` never reaches its 0.4 s pause. FreeBSD's `pipe_ioctl` answers `TIOCGPGRP` and `TIOCSPGRP` on a pipe or FIFO with the descriptor's SIGIO owner, which is not modelled, so `tcgetpgrp` and `tcsetpgrp` throw on a host pipe or FIFO above descriptor 2; descriptors 0 to 2 are taken as non-terminals that are not pipes, whatever the host attaches to them, so a pipe the guest creates on a freed descriptor 0 to 2 gets `ENOTTY` there. The PS5 `libkernel` is assumed to use this FreeBSD code, and `tcdrain` is not a cancellation point here - [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` 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 - [Windows file identity](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails From 0a353f80ef6b926c87084ee8e9dbcf65d6df389a Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:29:14 -0700 Subject: [PATCH 3/6] fix(libkernel): queue semaphore waiters by thread priority for SCE_KERNEL_SEMA_ATTR_TH_PRIO A semaphore created with attribute 0x02 inserts a waiter before the first queued waiter with a lower priority, so the signal hands tokens to higher-priority threads first and to equal ones in arrival order. guest_semaphore_handoff also checks two waiters: the skip of one that needs more than is left, and the order for both attributes. --- .../libkernel/Semaphore/include/Semaphore.hpp | 2 + .../prx/libkernel/Semaphore/src/Semaphore.cpp | 12 ++- core/libs/tests/GuestSemaphoreHandoff.cpp | 98 +++++++++++++++++-- core/libs/tests/SemaphoreTrace.cpp | 2 + docs/dev/debt/libkernel.md | 2 +- 5 files changed, 107 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libkernel/Semaphore/include/Semaphore.hpp b/core/libs/prx/libkernel/Semaphore/include/Semaphore.hpp index 3157e23a61..7cba2c8da9 100644 --- a/core/libs/prx/libkernel/Semaphore/include/Semaphore.hpp +++ b/core/libs/prx/libkernel/Semaphore/include/Semaphore.hpp @@ -17,6 +17,7 @@ constexpr int KERNEL_SEMA_OK = 0; struct KernelSemaWaiter { std::int32_t need; bool released = false; + int priority = 0; }; struct KernelSemaPrivate { @@ -29,6 +30,7 @@ struct KernelSemaPrivate { std::int32_t initCount; std::int32_t maxCount; bool isFifo; + bool priorityQueue = false; bool deleted = false; std::int32_t waiterCount = 0; std::list queue; diff --git a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp index 1112dc03ed..bcfbb5e056 100644 --- a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp +++ b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/Semaphore/include/Semaphore.hpp" #include "prx/libkernel/Time/include/Time.hpp" +#include #include #include #include @@ -12,6 +13,9 @@ KernelSemaPrivate::KernelSemaPrivate(std::int32_t initCount, std::int32_t maxCou : name(std::move(name)), tokenCount(initCount), initCount(initCount), maxCount(maxCount), isFifo(isFifo) { } +extern "C" Pthread APS5_VABI scePthreadSelf(); +extern "C" int APS5_VABI scePthreadGetprio(Pthread thread, int* prio); + static bool TraceSync() { static const bool enabled = std::getenv("APS5_TRACE_SYNC") != nullptr; return enabled; @@ -26,6 +30,7 @@ int APS5_VABI sceKernelCreateSema(KernelSema* sem, const char* name, uint32_t at } *sem = new KernelSemaPrivate(init, max, std::string(name), attr == 1); + (*sem)->priorityQueue = attr == 2; if (TraceSync()) std::fprintf(stderr, "[sema] create %p '%s' attr=0x%x init=%d max=%d\n", static_cast(*sem), name, attr, init, max); return KERNEL_SEMA_OK; } @@ -87,7 +92,12 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time) } const std::uint64_t generation = sem->cancelGeneration; KernelSemaWaiter waiter{need}; - sem->queue.push_back(&waiter); + if (sem->priorityQueue && scePthreadGetprio(scePthreadSelf(), &waiter.priority) == 0) { + const auto lower = std::find_if(sem->queue.begin(), sem->queue.end(), [&](const KernelSemaWaiter* queued) { return queued->priority > waiter.priority; }); + sem->queue.insert(lower, &waiter); + } else { + sem->queue.push_back(&waiter); + } ++sem->waiterCount; struct WaiterGuard { KernelSemaPrivate* sem; diff --git a/core/libs/tests/GuestSemaphoreHandoff.cpp b/core/libs/tests/GuestSemaphoreHandoff.cpp index c879b6feb8..4a58552588 100644 --- a/core/libs/tests/GuestSemaphoreHandoff.cpp +++ b/core/libs/tests/GuestSemaphoreHandoff.cpp @@ -1,4 +1,4 @@ -#include "SceTypes.hpp" +#include "prx/libkernel/Semaphore/include/Semaphore.hpp" #include #include #include @@ -8,15 +8,13 @@ extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI scePthreadJoin(Pthread thread, void** retval); -int APS5_VABI sceKernelCreateSema(KernelSema* sem, const char* name, uint32_t attr, int init, int max, void* opt); -int APS5_VABI sceKernelDeleteSema(KernelSema sem); -int APS5_VABI sceKernelPollSema(KernelSema sem, int need); -int APS5_VABI sceKernelSignalSema(KernelSema sem, int count); -int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time); +Pthread APS5_VABI scePthreadSelf(); +int APS5_VABI scePthreadSetprio(Pthread thread, int prio); } static constexpr int SCE_OK = 0; -static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); +static constexpr std::uint32_t SemaFifo = 1; +static constexpr std::uint32_t SemaPriority = 2; static void Require(bool value) { if (!value) std::abort(); } @@ -34,6 +32,86 @@ static void* APS5_VABI Wait(void* arg) { return nullptr; } +static std::atomic released{0}; + +static int QueuedCount(KernelSema sem) { + std::lock_guard lock(sem->mutex); + return static_cast(sem->queue.size()); +} + +struct QueuedWaiter { + KernelSema sem = nullptr; + int need = 1; + int priority = 0; + int result = -1; + std::atomic order{0}; +}; + +static void* APS5_VABI WaitInQueue(void* arg) { + auto& waiter = *static_cast(arg); + if (waiter.priority != 0) Require(scePthreadSetprio(scePthreadSelf(), waiter.priority) == SCE_OK); + KernelUseconds timeout = 5000000; + waiter.result = sceKernelWaitSema(waiter.sem, waiter.need, &timeout); + waiter.order = ++released; + return nullptr; +} + +static void WaitForQueue(KernelSema sem, int count) { + for (int i = 0; i < 5000 && QueuedCount(sem) != count; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(QueuedCount(sem) == count); +} + +static void WaitForReleased(int count) { + for (int i = 0; i < 5000 && released.load() < count; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(released.load() == count); +} + +static Pthread StartQueued(QueuedWaiter& waiter, int queued) { + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, WaitInQueue, &waiter, nullptr) == SCE_OK); + WaitForQueue(waiter.sem, queued); + return thread; +} + +static void CheckPassesOverALargerNeed() { + released = 0; + KernelSema sem = nullptr; + Require(sceKernelCreateSema(&sem, "handoff-skip", SemaFifo, 0, 4, nullptr) == SCE_OK); + QueuedWaiter large{sem, 2}; + QueuedWaiter small{sem, 1}; + const Pthread largeThread = StartQueued(large, 1); + const Pthread smallThread = StartQueued(small, 2); + Require(sceKernelSignalSema(sem, 1) == SCE_OK); + WaitForReleased(1); + Require(small.order == 1 && small.result == SCE_OK); + Require(QueuedCount(sem) == 1 && sceKernelPollSema(sem, 1) == SCE_KERNEL_ERROR_EBUSY); + Require(sceKernelSignalSema(sem, 2) == SCE_OK); + WaitForReleased(2); + Require(large.order == 2 && large.result == SCE_OK); + Require(scePthreadJoin(largeThread, nullptr) == SCE_OK && scePthreadJoin(smallThread, nullptr) == SCE_OK); + Require(sceKernelPollSema(sem, 1) == SCE_KERNEL_ERROR_EBUSY); + Require(sceKernelDeleteSema(sem) == SCE_OK); +} + +static void CheckServesInOrder(std::uint32_t attr, int firstPriority, int secondPriority, bool firstServedFirst) { + released = 0; + KernelSema sem = nullptr; + Require(sceKernelCreateSema(&sem, "handoff-order", attr, 0, 2, nullptr) == SCE_OK); + QueuedWaiter first{sem, 1, firstPriority}; + QueuedWaiter second{sem, 1, secondPriority}; + const Pthread firstThread = StartQueued(first, 1); + const Pthread secondThread = StartQueued(second, 2); + Require(sceKernelSignalSema(sem, 1) == SCE_OK); + WaitForReleased(1); + Require((firstServedFirst ? first.order : second.order) == 1); + Require(QueuedCount(sem) == 1); + Require(sceKernelSignalSema(sem, 1) == SCE_OK); + WaitForReleased(2); + Require(first.result == SCE_OK && second.result == SCE_OK); + Require(scePthreadJoin(firstThread, nullptr) == SCE_OK && scePthreadJoin(secondThread, nullptr) == SCE_OK); + Require(sceKernelDeleteSema(sem) == SCE_OK); +} + int main() { for (int i = 0; i < 20; ++i) { KernelSema sem = nullptr; @@ -50,5 +128,11 @@ int main() { Require(sceKernelPollSema(sem, 1) == SCE_KERNEL_ERROR_EBUSY); Require(sceKernelDeleteSema(sem) == SCE_OK); } + CheckPassesOverALargerNeed(); + CheckServesInOrder(SemaFifo, 700, 700, true); + CheckServesInOrder(SemaFifo, 767, 256, true); + CheckServesInOrder(SemaPriority, 767, 256, false); + CheckServesInOrder(SemaPriority, 256, 767, true); + CheckServesInOrder(SemaPriority, 700, 700, true); return 0; } diff --git a/core/libs/tests/SemaphoreTrace.cpp b/core/libs/tests/SemaphoreTrace.cpp index e49947b56e..c8488fd53f 100644 --- a/core/libs/tests/SemaphoreTrace.cpp +++ b/core/libs/tests/SemaphoreTrace.cpp @@ -5,6 +5,8 @@ #include extern "C" void KernelTraceWait_nid_postfix(const char*, const void*, std::uint64_t, bool) {} +extern "C" Pthread APS5_VABI scePthreadSelf() { return nullptr; } +extern "C" int APS5_VABI scePthreadGetprio(Pthread, int*) { return -1; } int main() { KernelSema sem = nullptr; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6431cf8979..8a1a0a4aec 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -125,7 +125,7 @@ - [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus `EVFILT_READ` on the read end and `EVFILT_WRITE` on the write end of a pipe (see the kevent on a pipe entry) and the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters and a read event on the host stream on descriptor 0, which never triggers, as `select` never reports descriptor 0 readable; PPSA34015 registers descriptor 0 for reading in a new kqueue at start-up and does not check the result; other filters, `EV_CLEAR` on a socket or a pipe off Linux (see the kevent EV_CLEAR entry), a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro) and any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) throw. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop. `NOTE_LOWAT` on a socket sets the low-water mark as FreeBSD 11's [`filt_soread` and `filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_socket.c) do, and a later change of the knote replaces it as `kqueue_register` replaces `kn_sdata`: a read event fires once the buffered bytes reach the mark, or at end of file or on a listening socket. A read mark above 1 is taken on stream sockets only: on a datagram socket Linux's `FIONREAD` reports the next datagram where FreeBSD's `sbavail` counts every queued byte, so it throws. A write event fires once the send buffer's free space reaches the mark, as `filt_sowrite` does with `sbspace`; the host reports that space as `SO_SNDBUF` less `SIOCOUTQ` on Linux and as `SO_SNDBUF` alone elsewhere, so off Linux it does not shrink while data is queued. Marks below 1, which make FreeBSD's read filter always fire, throw - [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. The three Windows notifications are told apart in the error, which names the `parent`, `entries` or `subdirectories` watch, the path it was given and `GetLastError`, but a `FindFirstChangeNotification` that fails still throws rather than returning an error to the guest: the EVFILT_VNODE attach path is [`vfs_kqfilter`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L4936), which returns `EINVAL`, `ENOMEM` or 0 after `fget` has turned a descriptor that is not open into `EBADF`, and consults no directory permission, so no FreeBSD errno stands for a host that refuses to notify, and what the console does on such a host is not measured. Only the `parent` watch can fail this way in practice: `entries` and `subdirectories` need the `FILE_LIST_DIRECTORY` that opening the watched directory already took. The parent handle produces no hint of its own, as `RunHostWatch` only calls `FindNextChangeNotification` on it, so a later change may leave the watch on its 50 ms poll instead of throwing; that is not done here. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title (feat(libkernel): implement kevent EV_DISABLE, EV_DISPATCH and EV_FORCEONESHOT) (fix(libkernel): follow kqueue_register for EV_FORCEONESHOT and modify flags) - [sem_timedwait](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. -- [sceKernelWaitSema and sceKernelSignalSema](../../../core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp) (libkernel) queue waiters in arrival order. `sceKernelSignalSema` hands its tokens to them in that order, passing over one that needs more than is left, and a `sceKernelWaitSema` or `sceKernelPollSema` that finds enough tokens takes them without queueing, as shadPS4's [semaphore](https://github.com/shadps4-emu/shadPS4/blob/06e813ff8c8e2a6d81b02cabfcc9543c3debf78e/src/core/libraries/kernel/threads/semaphore.cpp#L159-L206) does. `SCE_KERNEL_SEMA_ATTR_TH_PRIO` (0x02), for which shadPS4 orders the queue by thread priority, is not modelled, and the console's order is unverified +- [sceKernelWaitSema and sceKernelSignalSema](../../../core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp) (libkernel) queue waiters in arrival order, or with `SCE_KERNEL_SEMA_ATTR_TH_PRIO` (0x02) by the priority the waiting thread has when it starts to wait, lower values first and equal ones in arrival order. `sceKernelSignalSema` hands its tokens to them in that order, passing over one that needs more than is left, and a `sceKernelWaitSema` or `sceKernelPollSema` that finds enough tokens takes them without queueing, as shadPS4's [semaphore](https://github.com/shadps4-emu/shadPS4/blob/06e813ff8c8e2a6d81b02cabfcc9543c3debf78e/src/core/libraries/kernel/threads/semaphore.cpp#L159-L206) does. A priority change while a thread waits does not move it in the queue, an attribute of 0 queues in arrival order, and the console's order and its rule for a waiter that needs more than is left are unverified - [tcsendbreak, tcdrain, tcflush, tcflow, tcgetpgrp, tcsetpgrp](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) follow FreeBSD 11.4's [`termios.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/termios.c): `tcflush` answers `EINVAL` for a queue other than `TCIFLUSH`, `TCOFLUSH` or `TCIOFLUSH` and `tcflow` for an action outside `TCOOFF`..`TCION` before the descriptor is looked at, and every other call fails as its terminal ioctl does on a descriptor that is not a terminal, with the errors `tcgetattr` gives, so `tcsendbreak` never reaches its 0.4 s pause. FreeBSD's `pipe_ioctl` answers `TIOCGPGRP` and `TIOCSPGRP` on a pipe or FIFO with the descriptor's SIGIO owner, which is not modelled, so `tcgetpgrp` and `tcsetpgrp` throw on a host pipe or FIFO above descriptor 2; descriptors 0 to 2 are taken as non-terminals that are not pipes, whatever the host attaches to them, so a pipe the guest creates on a freed descriptor 0 to 2 gets `ENOTTY` there. The PS5 `libkernel` is assumed to use this FreeBSD code, and `tcdrain` is not a cancellation point here - [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` 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`. Directory timestamp updates resolve the current path of the opened handle. Filesystems with coarser timestamp resolution cannot retain every guest microsecond - [EVFILT_VNODE on macOS](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) registers `EVFILT_VNODE` on a duplicate of the descriptor in a host kqueue, whose notes have FreeBSD's values, and drops the `NOTE_LINK` that XNU adds to `NOTE_DELETE` when the watched file or directory is removed, since FreeBSD's `vop_remove_post` and `vop_rmdir_post` report `NOTE_DELETE` alone. As on Windows, a directory registered with `NOTE_EXTEND` is accepted but never receives it: XNU reports a move between directories as `NOTE_WRITE` alone, with `NOTE_LINK` for a subdirectory From 2132238f4539d6a1b98bade9c0d7a7ddfbdb05ef Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:15:26 -0700 Subject: [PATCH 4/6] fix(libkernel): refuse a wait on a semaphore being deleted before taking free tokens sceKernelDeleteSema sets the semaphore's deleted flag and waits for its waiters to leave before freeing it. A sceKernelWaitSema that arrived in that window with enough tokens took them on the fast path and returned SCE_OK without counting as a waiter, where main returns SCE_KERNEL_ERROR_EACCES. The deleted check now comes before the fast path, as main orders it. --- .../prx/libkernel/Semaphore/src/Semaphore.cpp | 4 ++++ core/libs/tests/GuestSemaphoreHandoff.cpp | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp index 7e2fd29fa6..d3c81d9799 100644 --- a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp +++ b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp @@ -101,6 +101,10 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time) const auto traceSyncWaitEnd = [&](int result) { if (TraceSync()) std::fprintf(stderr, "[sema] wait end %p '%s' result=0x%x have=%d caller=%p\n", static_cast(sem), sem->name.c_str(), result, sem->tokenCount, caller); }; + if (sem->deleted) { + traceSyncWaitEnd(SCE_KERNEL_ERROR_EACCES); + return SCE_KERNEL_ERROR_EACCES; + } if (sem->tokenCount >= need) { sem->tokenCount -= need; traceSyncWaitEnd(KERNEL_SEMA_OK); diff --git a/core/libs/tests/GuestSemaphoreHandoff.cpp b/core/libs/tests/GuestSemaphoreHandoff.cpp index 4a58552588..816225a844 100644 --- a/core/libs/tests/GuestSemaphoreHandoff.cpp +++ b/core/libs/tests/GuestSemaphoreHandoff.cpp @@ -93,6 +93,22 @@ static void CheckPassesOverALargerNeed() { Require(sceKernelDeleteSema(sem) == SCE_OK); } +static void CheckDeletedRefusesFreeTokens() { + KernelSema sem = nullptr; + Require(sceKernelCreateSema(&sem, "handoff-deleted", SemaFifo, 2, 2, nullptr) == SCE_OK); + { + std::lock_guard lock(sem->mutex); + sem->deleted = true; + } + Require(sceKernelWaitSema(sem, 1, nullptr) == SCE_KERNEL_ERROR_EACCES); + { + std::lock_guard lock(sem->mutex); + Require(sem->tokenCount == 2); + sem->deleted = false; + } + Require(sceKernelDeleteSema(sem) == SCE_OK); +} + static void CheckServesInOrder(std::uint32_t attr, int firstPriority, int secondPriority, bool firstServedFirst) { released = 0; KernelSema sem = nullptr; @@ -129,6 +145,7 @@ int main() { Require(sceKernelDeleteSema(sem) == SCE_OK); } CheckPassesOverALargerNeed(); + CheckDeletedRefusesFreeTokens(); CheckServesInOrder(SemaFifo, 700, 700, true); CheckServesInOrder(SemaFifo, 767, 256, true); CheckServesInOrder(SemaPriority, 767, 256, false); From 8b1608d2de3757d3411d6f42656b765c42aa9ec6 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 29 Oct 2026 01:29:45 -0700 Subject: [PATCH 5/6] refactor(libkernel): take the semaphore code's pthread declarations from Api.hpp Semaphore.cpp and the handoff test declared scePthreadSelf, scePthreadGetprio, scePthreadCreate, scePthreadJoin and scePthreadSetprio locally. libkernel's Api.hpp already declares them with the same signatures, so both include it instead. The test keeps the internal semaphore header for the fixture state. --- core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp | 4 +--- core/libs/tests/GuestSemaphoreHandoff.cpp | 8 +------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp index d3c81d9799..41a6eeb2f3 100644 --- a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp +++ b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp @@ -1,4 +1,5 @@ #include "prx/libkernel/Semaphore/include/Semaphore.hpp" +#include "prx/libkernel/include/Api.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include "prx/libc/include/General.hpp" #include @@ -14,9 +15,6 @@ KernelSemaPrivate::KernelSemaPrivate(std::int32_t initCount, std::int32_t maxCou : name(std::move(name)), tokenCount(initCount), initCount(initCount), maxCount(maxCount), isFifo(isFifo) { } -extern "C" Pthread APS5_VABI scePthreadSelf(); -extern "C" int APS5_VABI scePthreadGetprio(Pthread thread, int* prio); - static bool TraceSync() { static const bool enabled = std::getenv("APS5_TRACE_SYNC") != nullptr; return enabled; diff --git a/core/libs/tests/GuestSemaphoreHandoff.cpp b/core/libs/tests/GuestSemaphoreHandoff.cpp index 816225a844..d6a7708803 100644 --- a/core/libs/tests/GuestSemaphoreHandoff.cpp +++ b/core/libs/tests/GuestSemaphoreHandoff.cpp @@ -1,17 +1,11 @@ #include "prx/libkernel/Semaphore/include/Semaphore.hpp" +#include "prx/libkernel/include/Api.hpp" #include #include #include #include #include -extern "C" { -int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); -int APS5_VABI scePthreadJoin(Pthread thread, void** retval); -Pthread APS5_VABI scePthreadSelf(); -int APS5_VABI scePthreadSetprio(Pthread thread, int prio); -} - static constexpr int SCE_OK = 0; static constexpr std::uint32_t SemaFifo = 1; static constexpr std::uint32_t SemaPriority = 2; From c87a1fb54e9d61bf1edc4ad652ab3a22bc296e24 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 29 Oct 2026 01:30:17 -0700 Subject: [PATCH 6/6] test(libkernel): wait until the handoff waiter is queued before signalling The first handoff loop waited for the worker to set started and then slept 50 ms, but started is set before sceKernelWaitSema queues the worker, so a delayed worker could still be unqueued at the signal. The loop now waits with WaitForQueue(sem, 1), as the other cases do, and Waiter drops the started flag. --- core/libs/tests/GuestSemaphoreHandoff.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/libs/tests/GuestSemaphoreHandoff.cpp b/core/libs/tests/GuestSemaphoreHandoff.cpp index d6a7708803..24b04abb4a 100644 --- a/core/libs/tests/GuestSemaphoreHandoff.cpp +++ b/core/libs/tests/GuestSemaphoreHandoff.cpp @@ -15,13 +15,11 @@ static void Require(bool value) { if (!value) std::abort(); } struct Waiter { KernelSema sem = nullptr; int result = -1; - std::atomic started{false}; }; static void* APS5_VABI Wait(void* arg) { auto& waiter = *static_cast(arg); KernelUseconds timeout = 1000000; - waiter.started = true; waiter.result = sceKernelWaitSema(waiter.sem, 1, &timeout); return nullptr; } @@ -129,8 +127,7 @@ int main() { Waiter waiter{sem}; Pthread thread = nullptr; Require(scePthreadCreate(&thread, nullptr, Wait, &waiter, nullptr) == SCE_OK); - while (!waiter.started) std::this_thread::yield(); - std::this_thread::sleep_for(std::chrono::milliseconds(50)); + WaitForQueue(sem, 1); Require(sceKernelSignalSema(sem, 1) == SCE_OK); Require(sceKernelPollSema(sem, 1) == SCE_KERNEL_ERROR_EBUSY); Require(scePthreadJoin(thread, nullptr) == SCE_OK);