diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index a9fc644954..506333c24e 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -123,6 +124,7 @@ int APS5_VABI sceUltInitialize() { int APS5_VABI sceUltFinalize() { std::vector> semaphores; + std::vector> mutexes; { std::lock_guard lock(gMutex); if (!gUlthreads.empty()) { @@ -136,6 +138,13 @@ int APS5_VABI sceUltFinalize() { semaphores.push_back(state); } gSemaphores.clear(); + mutexes.reserve(gMutexes.size()); + for (auto& entry : gMutexes) { + auto& state = entry.second; + std::lock_guard stateLock(state->_mutex); + state->_alive = false; + mutexes.push_back(state); + } gMutexes.clear(); gResourcePools.clear(); for (auto& entry : gQueues) { @@ -152,6 +161,9 @@ int APS5_VABI sceUltFinalize() { for (const auto& state : semaphores) { state->_available.notify_all(); } + for (const auto& state : mutexes) { + state->_available.notify_all(); + } return ULT_OK; } @@ -172,15 +184,38 @@ int APS5_VABI sceUltMutexCreate(void* mutex, const char* name, void* waitingQueu } auto state = std::make_shared(); state->_attribute = optParam != nullptr ? optParam->attribute : 0; + state->_waitingPool = waitingQueueResourcePool; std::lock_guard lock(gMutex); if (waitingQueueResourcePool != nullptr && gResourcePools.find(waitingQueueResourcePool) == gResourcePools.end()) { return ULT_ERROR_INVALID; } + if (gMutexes.find(mutex) != gMutexes.end()) { + return ULT_ERROR_STATE; + } std::memset(mutex, 0, 256); gMutexes[mutex] = std::move(state); return ULT_OK; } +int APS5_VABI sceUltMutexDestroy(void* mutex) { + if (mutex == nullptr) { + return ULT_ERROR_NULL; + } + std::lock_guard lock(gMutex); + auto it = gMutexes.find(mutex); + if (it == gMutexes.end()) { + return ULT_ERROR_STATE; + } + auto state = it->second; + std::lock_guard stateLock(state->_mutex); + if (state->_lockCount != 0 || state->_waiters != 0) { + return ULT_ERROR_BUSY; + } + state->_alive = false; + gMutexes.erase(it); + return ULT_OK; +} + int APS5_VABI sceUltMutexLock(void* mutex) { std::shared_ptr state; { @@ -191,7 +226,30 @@ int APS5_VABI sceUltMutexLock(void* mutex) { } state = it->second; } - state->_mutex.lock(); + const auto self = std::this_thread::get_id(); + std::unique_lock lock(state->_mutex); + if (!state->_alive) { + return ULT_ERROR_STATE; + } + if (state->_owner == self) { + ++state->_lockCount; + return ULT_OK; + } + ++state->_waiters; + try { + state->_available.wait(lock, [&] { + return !state->_alive || state->_lockCount == 0; + }); + } catch (...) { + --state->_waiters; + throw; + } + --state->_waiters; + if (!state->_alive) { + return ULT_ERROR_STATE; + } + state->_owner = self; + state->_lockCount = 1; return ULT_OK; } @@ -205,7 +263,16 @@ int APS5_VABI sceUltMutexUnlock(void* mutex) { } state = it->second; } - state->_mutex.unlock(); + const auto self = std::this_thread::get_id(); + std::lock_guard lock(state->_mutex); + if (!state->_alive || state->_owner != self) { + return ULT_ERROR_STATE; + } + --state->_lockCount; + if (state->_lockCount == 0) { + state->_owner = {}; + state->_available.notify_one(); + } return ULT_OK; } @@ -226,6 +293,43 @@ int APS5_VABI sceUltWaitingQueueResourcePoolCreate(void* pool, const char* name, return ULT_OK; } +int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void* pool) { + if (pool == nullptr) { + return ULT_ERROR_NULL; + } + std::lock_guard lock(gMutex); + auto it = gResourcePools.find(pool); + if (it == gResourcePools.end()) { + return ULT_ERROR_STATE; + } + for (const auto& entry : gMutexes) { + if (entry.second->_waitingPool == pool) { + std::lock_guard stateLock(entry.second->_mutex); + if (entry.second->_waiters != 0) { + return ULT_ERROR_BUSY; + } + } + } + for (const auto& entry : gSemaphores) { + if (entry.second->_waitingPool == pool) { + std::lock_guard stateLock(entry.second->_mutex); + if (entry.second->_waiters != 0) { + return ULT_ERROR_BUSY; + } + } + } + for (const auto& entry : gQueues) { + if (entry.second->_pool && entry.second->_pool->_waitingPool == pool) { + std::lock_guard poolLock(entry.second->_pool->_mutex); + if (entry.second->_waiters != 0) { + return ULT_ERROR_BUSY; + } + } + } + gResourcePools.erase(it); + return ULT_OK; +} + std::uint64_t APS5_VABI sceUltQueueDataResourcePoolGetWorkAreaSize(std::uint32_t numData, std::uint64_t dataSize, std::uint32_t numQueueObject) { if (dataSize > UINT64_MAX - 7u) throw std::out_of_range("ULT queue data size overflow"); const auto stride = alignUp(dataSize, 8u); @@ -360,6 +464,7 @@ int APS5_VABI sceUltSemaphoreCreate(void* semaphore, const char* name, std::int3 } auto state = std::make_shared(); state->_resources = numInitialResource; + state->_waitingPool = waitingQueueResourcePool; std::lock_guard lock(gMutex); if (waitingQueueResourcePool != nullptr && gResourcePools.find(waitingQueueResourcePool) == gResourcePools.end()) { return ULT_ERROR_INVALID; diff --git a/core/libs/prx/libSceUlt/SceUltTypes.hpp b/core/libs/prx/libSceUlt/SceUltTypes.hpp index 0a314a9d56..995972b952 100644 --- a/core/libs/prx/libSceUlt/SceUltTypes.hpp +++ b/core/libs/prx/libSceUlt/SceUltTypes.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "SceTypes.hpp" @@ -19,8 +20,14 @@ constexpr int ULT_ERROR_BUSY = -2139029497; constexpr int ULT_ERROR_AGAIN = -2139029496; struct UltMutexState { - std::recursive_mutex _mutex; + std::mutex _mutex; + std::condition_variable _available; + std::thread::id _owner{}; + std::uint32_t _lockCount = 0; + std::uint32_t _waiters = 0; + bool _alive = true; std::uint32_t _attribute = 0; + void* _waitingPool = nullptr; }; struct UltSemaphoreState { @@ -29,6 +36,7 @@ struct UltSemaphoreState { std::int32_t _resources = 0; std::uint32_t _waiters = 0; bool _alive = true; + void* _waitingPool = nullptr; }; struct UltResourcePoolState { diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp index fdf347e64e..b076be3735 100644 --- a/core/libs/tests/GuestUltThread.cpp +++ b/core/libs/tests/GuestUltThread.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include extern "C" { @@ -30,6 +31,11 @@ int APS5_VABI _sceUltQueueDataResourcePoolCreate(void*, const char*, std::uint32 int APS5_VABI _sceUltQueueCreate(void*, const char*, std::uint64_t, void*, void*, const void*, std::uint32_t); int APS5_VABI sceUltQueuePush(void*, const void*); int APS5_VABI sceUltQueuePop(void*, void*); +int APS5_VABI sceUltMutexDestroy(void*); +int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void*); +int APS5_VABI sceUltSemaphoreDestroy(void*); +int APS5_VABI sceUltQueueDestroy(void*); +int APS5_VABI sceUltQueueDataResourcePoolDestroy(void*); } static constexpr int Ok = 0; @@ -177,6 +183,34 @@ static void UnderscoredCreate() { std::uint64_t value = 0x1122334455667788ull, output = 0; CHECK(sceUltQueuePush(queue, &value) == Ok); CHECK(sceUltQueuePop(queue, &output) == Ok && output == value); + CHECK(sceUltWaitingQueueResourcePoolDestroy(nullptr) == Null); + CHECK(sceUltWaitingQueueResourcePoolDestroy(unknownPool) == State); + CHECK(sceUltMutexDestroy(nullptr) == Null); + CHECK(sceUltMutexLock(mutex) == Ok); + CHECK(sceUltMutexDestroy(mutex) == Busy); + std::atomic waiterStarted{false}; + std::atomic waiterAcquired{false}; + std::thread waiter([&] { + waiterStarted.store(true, std::memory_order_release); + CHECK(sceUltMutexLock(mutex) == Ok); + waiterAcquired.store(true, std::memory_order_release); + CHECK(sceUltMutexUnlock(mutex) == Ok); + }); + while (!waiterStarted.load(std::memory_order_acquire)) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + CHECK(!waiterAcquired.load(std::memory_order_acquire)); + CHECK(sceUltMutexDestroy(mutex) == Busy); + CHECK(sceUltWaitingQueueResourcePoolDestroy(pool) == Busy); + CHECK(sceUltMutexUnlock(mutex) == Ok); + waiter.join(); + CHECK(waiterAcquired.load(std::memory_order_acquire)); + CHECK(sceUltWaitingQueueResourcePoolDestroy(pool) == Ok); + CHECK(sceUltWaitingQueueResourcePoolDestroy(pool) == State); + CHECK(sceUltMutexDestroy(mutex) == Ok); + CHECK(sceUltMutexDestroy(mutex) == State); + CHECK(sceUltSemaphoreDestroy(semaphore) == Ok); + CHECK(sceUltQueueDestroy(queue) == Ok); + CHECK(sceUltQueueDataResourcePoolDestroy(dataPool) == Ok); } int main(int argc, char** argv) { diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index e1fb5ff21a..3ac4a39278 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -9,3 +9,5 @@ ### Functional - [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`. +- [sceUltWaitingQueueResourcePoolDestroy](../../../core/libs/prx/libSceUlt/Export.cpp) refuses with `ULT_ERROR_BUSY` only when a thread is currently waiting on a pool-backed mutex, semaphore or queue, matching the PS5 libSceUlt disassembly (0x473a in or55417wcDk); bound but idle objects do not block pool destruction. Waiting-queue pool guest work area node accounting is not implemented. +- [sceUltMutexDestroy](../../../core/libs/prx/libSceUlt/Export.cpp) returns `ULT_ERROR_BUSY` when the mutex is held or has waiting threads, tracked via owner and waiter counts in `UltMutexState`. Prosper (`hle_ult.cpp`) logs a destroy of a held mutex and returns `ULT_OK`; AnyPS5 returns `ULT_ERROR_BUSY` to prevent destroying active mutexes.