From dab11105566597c4de33a13d2fe0c9108a68b786 Mon Sep 17 00:00:00 2001 From: DotDebian Date: Fri, 2 Oct 2026 11:57:00 +0200 Subject: [PATCH] feat(libkernel): implement sceKernelCancelSema sceKernelCancelSema was a throwing stub. It now releases every thread blocked in sceKernelWaitSema with SCE_KERNEL_ERROR_ECANCELED without consuming tokens, sets the count to setCount (the initial count when setCount is negative) and reports how many threads were waiting. A setCount above the maximum returns SCE_KERNEL_ERROR_EINVAL and changes nothing. Waiters are counted separately from waiterCount, which is only decremented when a waiter leaves, so two cancels in a row do not report the same thread twice. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 10 +++ .../libkernel/Semaphore/include/Semaphore.hpp | 4 + .../prx/libkernel/Semaphore/src/Semaphore.cpp | 42 +++++++--- core/libs/tests/GuestSemaphore.cpp | 81 +++++++++++++++++++ 4 files changed, 128 insertions(+), 9 deletions(-) create mode 100644 core/libs/tests/GuestSemaphore.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 907294a56..cdd441675 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -145,6 +145,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_sem 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) + configure_windows_unwind(guest_semaphore_tests) + add_test(NAME guest_semaphore COMMAND guest_semaphore_tests) + set_tests_properties(guest_semaphore PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_semaphore PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_signal_tests ${LIBS_INCLUDE_DIR}/tests/GuestSignals.cpp) target_include_directories(guest_signal_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_signal_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 774d1f909..08c050597 100644 --- a/core/libs/prx/libkernel/Semaphore/include/Semaphore.hpp +++ b/core/libs/prx/libkernel/Semaphore/include/Semaphore.hpp @@ -20,10 +20,13 @@ struct KernelSemaPrivate { TimedWait::Condition condition; std::string name; std::int32_t tokenCount; + std::int32_t initCount; std::int32_t maxCount; bool isFifo; bool deleted = false; std::int32_t waiterCount = 0; + std::int32_t cancelableCount = 0; + std::uint64_t cancelGeneration = 0; }; extern "C" { @@ -33,6 +36,7 @@ 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); +int APS5_VABI sceKernelCancelSema(KernelSema sem, int count, int* threads); } diff --git a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp index 6c5e07d24..3b11de2c3 100644 --- a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp +++ b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp @@ -7,7 +7,7 @@ #include KernelSemaPrivate::KernelSemaPrivate(std::int32_t initCount, std::int32_t maxCount, std::string name, bool isFifo) - : name(std::move(name)), tokenCount(initCount), maxCount(maxCount), isFifo(isFifo) { + : name(std::move(name)), tokenCount(initCount), initCount(initCount), maxCount(maxCount), isFifo(isFifo) { } extern "C" { @@ -55,34 +55,47 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time) } std::unique_lock lock(sem->mutex); + const std::uint64_t generation = sem->cancelGeneration; ++sem->waiterCount; + ++sem->cancelableCount; struct WaiterGuard { KernelSemaPrivate* sem; + std::uint64_t generation; ~WaiterGuard() { --sem->waiterCount; + if (sem->cancelGeneration == generation) { + --sem->cancelableCount; + } sem->condition.NotifyAll(); } - } waiterGuard{sem}; + } waiterGuard{sem, generation}; 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; }; if (time == nullptr) { - sem->condition.Wait(lock, [&] { return sem->tokenCount >= need || sem->deleted; }); + 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; return KERNEL_SEMA_OK; } - const bool acquired = sem->condition.WaitUntil(lock, TimedWait::DeadlineNanos(*time), [&] { return sem->tokenCount >= need || sem->deleted; }); + const bool acquired = sem->condition.WaitUntil(lock, TimedWait::DeadlineNanos(*time), released); 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; } @@ -91,11 +104,22 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time) } int APS5_VABI sceKernelCancelSema(KernelSema sem, int count, int* threads) { - (void)sem; - (void)count; - (void)threads; - NotImplemented_nid_no_patch(__func__); - return 0; + if (sem == nullptr) { + APS5_INVALID_ARG_EX; + } + + std::lock_guard lock(sem->mutex); + if (count > sem->maxCount) { + return SCE_KERNEL_ERROR_EINVAL; + } + if (threads != nullptr) { + *threads = sem->cancelableCount; + } + sem->tokenCount = count < 0 ? sem->initCount : count; + sem->cancelableCount = 0; + ++sem->cancelGeneration; + sem->condition.NotifyAll(); + return KERNEL_SEMA_OK; } int APS5_VABI sceKernelDeleteSema(KernelSema sem) { diff --git a/core/libs/tests/GuestSemaphore.cpp b/core/libs/tests/GuestSemaphore.cpp new file mode 100644 index 000000000..fd90d1c80 --- /dev/null +++ b/core/libs/tests/GuestSemaphore.cpp @@ -0,0 +1,81 @@ +#include "SceTypes.hpp" +#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); +int APS5_VABI sceKernelCancelSema(KernelSema sem, int count, int* threads); +} + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); +static constexpr int SCE_KERNEL_ERROR_ECANCELED = static_cast(0x80020055); + +static void Require(bool value) { if (!value) std::abort(); } + +struct Waiter { + KernelSema sem = nullptr; + bool timed = false; + int result = SCE_OK; +}; + +static void* APS5_VABI Wait(void* arg) { + auto& waiter = *static_cast(arg); + KernelUseconds timeout = 5000000; + waiter.result = sceKernelWaitSema(waiter.sem, 1, waiter.timed ? &timeout : nullptr); + return nullptr; +} + +int main() { + KernelSema sem = nullptr; + Require(sceKernelCreateSema(&sem, "cancel", 0, 1, 3, nullptr) == SCE_OK); + + int waiters = -1; + Require(sceKernelCancelSema(sem, 4, &waiters) == SCE_KERNEL_ERROR_EINVAL); + Require(waiters == -1); + Require(sceKernelPollSema(sem, 1) == SCE_OK); + Require(sceKernelPollSema(sem, 1) == SCE_KERNEL_ERROR_EBUSY); + + Require(sceKernelCancelSema(sem, 2, &waiters) == SCE_OK && waiters == 0); + Require(sceKernelPollSema(sem, 2) == SCE_OK); + Require(sceKernelPollSema(sem, 1) == SCE_KERNEL_ERROR_EBUSY); + + Require(sceKernelCancelSema(sem, -1, nullptr) == SCE_OK); + Require(sceKernelPollSema(sem, 1) == SCE_OK); + Require(sceKernelPollSema(sem, 1) == SCE_KERNEL_ERROR_EBUSY); + + Waiter blocked{sem, false}; + Waiter timed{sem, true}; + Pthread blockedThread = nullptr; + Pthread timedThread = nullptr; + Require(scePthreadCreate(&blockedThread, nullptr, Wait, &blocked, nullptr) == SCE_OK); + Require(scePthreadCreate(&timedThread, nullptr, Wait, &timed, nullptr) == SCE_OK); + + int canceled = 0; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(4); + while (canceled < 2 && std::chrono::steady_clock::now() < deadline) { + waiters = -1; + Require(sceKernelCancelSema(sem, 0, &waiters) == SCE_OK); + Require(waiters >= 0); + canceled += waiters; + } + Require(scePthreadJoin(blockedThread, nullptr) == SCE_OK); + Require(scePthreadJoin(timedThread, nullptr) == SCE_OK); + Require(canceled == 2); + Require(blocked.result == SCE_KERNEL_ERROR_ECANCELED); + Require(timed.result == SCE_KERNEL_ERROR_ECANCELED); + Require(sceKernelCancelSema(sem, 0, &waiters) == SCE_OK && waiters == 0); + Require(sceKernelPollSema(sem, 1) == SCE_KERNEL_ERROR_EBUSY); + + Require(sceKernelSignalSema(sem, 1) == SCE_OK); + Require(sceKernelWaitSema(sem, 1, nullptr) == SCE_OK); + Require(sceKernelDeleteSema(sem) == SCE_OK); +}