diff --git a/core/libs/prx/libSceUlt/CMakeLists.txt b/core/libs/prx/libSceUlt/CMakeLists.txt index 381db5903a..5736205384 100644 --- a/core/libs/prx/libSceUlt/CMakeLists.txt +++ b/core/libs/prx/libSceUlt/CMakeLists.txt @@ -54,9 +54,9 @@ if(BUILD_TESTING) add_test_executable(guest_ult_sync_tests ${LIBS_INCLUDE_DIR}/tests/GuestUltSync.cpp) target_include_directories(guest_ult_sync_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_ult_sync_tests PRIVATE libSceUlt libc) + target_link_libraries(guest_ult_sync_tests PRIVATE libSceUlt libc libkernel) configure_windows_unwind(guest_ult_sync_tests) - foreach(testCase IN ITEMS mutex-trylock opt-params rwlock-validation rwlock-readers rwlock-writer rwlock-pool rwlock-finalize ulthreads get-info queue-waiters) + foreach(testCase IN ITEMS mutex-trylock opt-params rwlock-validation rwlock-readers rwlock-writer rwlock-pool rwlock-finalize ulthreads get-info queue-waiters info-waiters) add_test(NAME guest_ult_sync_${testCase} COMMAND guest_ult_sync_tests ${testCase}) set_tests_properties(guest_ult_sync_${testCase} PROPERTIES TIMEOUT 20) if(WIN32) diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index 3f020b3f57..3b55ed03e2 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -133,6 +133,14 @@ void* APS5_VABI ulthreadRunner(void* arg) { } +UltThreadIdentity UltCurrentThreadIdentity_nid_no_patch() { + if (gCurrentUlthread != nullptr) return {}; + const auto self = scePthreadSelf(); + std::uintptr_t thread; + std::memcpy(&thread, self, sizeof(thread)); + return {reinterpret_cast(thread), 1}; +} + bool UltWaitingPoolExists(void* pool) { std::lock_guard lock(gMutex); return gResourcePools.contains(pool); @@ -323,6 +331,7 @@ int APS5_VABI sceUltMutexLock(void* mutex) { if (!state->_alive) { return ULT_ERROR_STATE; } + state->_ownerIdentity = UltCurrentThreadIdentity_nid_no_patch(); state->_owner = self; state->_lockCount = 1; return ULT_OK; @@ -350,6 +359,7 @@ int APS5_VABI sceUltMutexTryLock(void* mutex) { if (state->_lockCount != 0) { return ULT_ERROR_BUSY; } + state->_ownerIdentity = UltCurrentThreadIdentity_nid_no_patch(); state->_owner = self; state->_lockCount = 1; return ULT_OK; @@ -376,6 +386,7 @@ int APS5_VABI sceUltMutexUnlock(void* mutex) { --state->_lockCount; if (state->_lockCount == 0) { state->_owner = {}; + state->_ownerIdentity = {}; state->_available.notify_one(); } return ULT_OK; @@ -451,12 +462,14 @@ int APS5_VABI sceUltConditionVariableWait(void* condition) { std::unique_lock lock(held->_mutex); if (!state->_alive || !held->_alive || held->_owner != owner || held->_lockCount == 0) return ULT_ERROR_STATE; if (held->_lockCount != 1) throw std::runtime_error("ULT condition wait with a recursively held mutex is not implemented"); + const auto ownerIdentity = held->_ownerIdentity; UltConditionWaiter waiter; state->_queue.push_back(&waiter); ++state->_waiters; ++held->_conditionWaiters; held->_lockCount = 0; held->_owner = {}; + held->_ownerIdentity = {}; held->_available.notify_one(); try { waiter._available.wait(lock, [&] { return waiter._signaled || !state->_alive || !held->_alive; }); @@ -481,6 +494,7 @@ int APS5_VABI sceUltConditionVariableWait(void* condition) { --held->_conditionWaiters; if (!held->_alive || !state->_alive) return ULT_ERROR_STATE; held->_owner = owner; + held->_ownerIdentity = ownerIdentity; held->_lockCount = 1; return ULT_OK; } @@ -1029,17 +1043,13 @@ int APS5_VABI sceUltGetConditionVariableInfo(void* object, UltConditionVariableI } std::lock_guard stateLock(state->_mutex->_mutex); if (!state->_alive) return ULT_ERROR_STATE; + if (capacity != 0 && !state->_queue.empty()) NotImplemented_nid_no_patch(__func__); std::memset(info, 0, sizeof(*info)); info->size = sizeof(UltConditionVariableInfo); info->object = object; std::memcpy(info->name, state->_name, sizeof(info->name)); info->mutex = state->_mutexObject; info->waitingThreads = static_cast(state->_queue.size()); - const auto toCopy = std::min(capacity, info->waitingThreads); - for (std::uint32_t i = 0; i < toCopy; ++i) { - std::memset(&waitingThreads[i], 0, sizeof(UltWaitingThreadInfo)); - waitingThreads[i].size = sizeof(UltWaitingThreadInfo); - } return ULT_OK; } @@ -1055,19 +1065,20 @@ int APS5_VABI sceUltGetMutexInfo(void* object, UltMutexInfo* info, UltWaitingThr } std::lock_guard stateLock(state->_mutex); if (!state->_alive) return ULT_ERROR_STATE; + for (std::uint32_t index = 0; index < capacity; ++index) { + if (waitingThreads[index].size != sizeof(UltWaitingThreadInfo)) return ULT_ERROR_INVALID; + } + if (capacity != 0 && state->_waiters != 0) NotImplemented_nid_no_patch(__func__); + if (state->_lockCount != 0 && (state->_attribute != 0 || state->_lockCount > 1 || state->_ownerIdentity.type != 1)) NotImplemented_nid_no_patch(__func__); std::memset(info, 0, sizeof(*info)); info->size = sizeof(UltMutexInfo); info->object = object; std::memcpy(info->name, state->_name, sizeof(info->name)); info->waitingQueuePool = state->_waitingPool; + info->owner = state->_ownerIdentity.thread; + info->ownerType = state->_ownerIdentity.type; info->attributes = state->_attribute; - info->lockCount = state->_lockCount; info->waitingThreads = state->_waiters; - const auto toCopy = std::min(capacity, info->waitingThreads); - for (std::uint32_t i = 0; i < toCopy; ++i) { - std::memset(&waitingThreads[i], 0, sizeof(UltWaitingThreadInfo)); - waitingThreads[i].size = sizeof(UltWaitingThreadInfo); - } return ULT_OK; } @@ -1111,6 +1122,7 @@ int APS5_VABI sceUltGetQueueInfo(void* object, UltQueueInfo* info, UltWaitingThr auto& pool = *state->_pool; std::lock_guard poolLock(pool._mutex); if (!state->_alive) return ULT_ERROR_STATE; + if (capacity != 0 && (state->_waitingPushers != 0 || state->_waitingPoppers != 0)) NotImplemented_nid_no_patch(__func__); std::memset(info, 0, sizeof(*info)); info->size = sizeof(UltQueueInfo); info->object = object; @@ -1121,12 +1133,6 @@ int APS5_VABI sceUltGetQueueInfo(void* object, UltQueueInfo* info, UltWaitingThr info->queuedItems = state->_queuedItems; info->waitingPushers = state->_waitingPushers; info->waitingPoppers = state->_waitingPoppers; - const auto waitingCount = info->waitingPushers + info->waitingPoppers; - const auto toCopy = std::min(capacity, waitingCount); - for (std::uint32_t i = 0; i < toCopy; ++i) { - std::memset(&waitingThreads[i], 0, sizeof(UltWaitingThreadInfo)); - waitingThreads[i].size = sizeof(UltWaitingThreadInfo); - } return ULT_OK; } @@ -1142,6 +1148,7 @@ int APS5_VABI sceUltGetSemaphoreInfo(void* object, UltSemaphoreInfo* info, UltWa } std::lock_guard stateLock(state->_mutex); if (!state->_alive) return ULT_ERROR_STATE; + if (capacity != 0 && state->_waiters != 0) NotImplemented_nid_no_patch(__func__); std::memset(info, 0, sizeof(*info)); info->size = sizeof(UltSemaphoreInfo); info->object = object; @@ -1149,11 +1156,6 @@ int APS5_VABI sceUltGetSemaphoreInfo(void* object, UltSemaphoreInfo* info, UltWa info->waitingQueuePool = state->_waitingPool; info->resources = static_cast(state->_resources); info->waitingThreads = state->_waiters; - const auto toCopy = std::min(capacity, info->waitingThreads); - for (std::uint32_t i = 0; i < toCopy; ++i) { - std::memset(&waitingThreads[i], 0, sizeof(UltWaitingThreadInfo)); - waitingThreads[i].size = sizeof(UltWaitingThreadInfo); - } return ULT_OK; } diff --git a/core/libs/prx/libSceUlt/SceUltTypes.hpp b/core/libs/prx/libSceUlt/SceUltTypes.hpp index be1bb08016..f218503bbc 100644 --- a/core/libs/prx/libSceUlt/SceUltTypes.hpp +++ b/core/libs/prx/libSceUlt/SceUltTypes.hpp @@ -265,10 +265,16 @@ static_assert(offsetof(UltUlthreadInfo, runtime) == 56); static_assert(offsetof(UltUlthreadInfo, fiber) == 64); static_assert(offsetof(UltUlthreadInfo, reserved) == 192); +struct UltThreadIdentity { + void* thread = nullptr; + std::uint32_t type = 0; +}; + struct UltMutexState { std::mutex _mutex; std::condition_variable _available; std::thread::id _owner{}; + UltThreadIdentity _ownerIdentity; std::uint32_t _lockCount = 0; std::uint32_t _waiters = 0; std::uint32_t _conditions = 0; diff --git a/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp b/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp index cc3d4458e4..489f3587ea 100644 --- a/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp +++ b/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp @@ -1,6 +1,7 @@ #include "prx/libSceUlt/include/Api.hpp" #include "prx/libSceUlt/src/ReaderWriterLock.hpp" #include "prx/libSceUlt/SceUltTypes.hpp" +#include "prx/libc/include/General.hpp" #include #include #include @@ -18,6 +19,7 @@ struct UltReaderWriterLockState { std::uint32_t _readers = 0; bool _writing = false; std::thread::id _writer{}; + UltThreadIdentity _ownerIdentity; std::uint32_t _waiters = 0; bool _alive = true; void* _waitingPool = nullptr; @@ -170,6 +172,7 @@ int APS5_VABI sceUltReaderWriterLockLockWrite(void* rwlock) { return ULT_ERROR_STATE; } if (const int waited = Wait(*state, lock, true); waited != ULT_OK) return waited; + state->_ownerIdentity = UltCurrentThreadIdentity_nid_no_patch(); state->_writing = true; state->_writer = std::this_thread::get_id(); return ULT_OK; @@ -185,6 +188,7 @@ int APS5_VABI sceUltReaderWriterLockTryLockWrite(void* rwlock) { if (state->_writing || state->_readers != 0) { return ULT_ERROR_BUSY; } + state->_ownerIdentity = UltCurrentThreadIdentity_nid_no_patch(); state->_writing = true; state->_writer = std::this_thread::get_id(); return ULT_OK; @@ -199,6 +203,7 @@ int APS5_VABI sceUltReaderWriterLockUnlockWrite(void* rwlock) { } state->_writing = false; state->_writer = {}; + state->_ownerIdentity = {}; state->_released.notify_all(); return ULT_OK; } @@ -210,6 +215,11 @@ int APS5_VABI sceUltGetReaderWriterLockInfo(void* object, UltReaderWriterLockInf if (const int found = FindLock(object, state); found != ULT_OK) return found; std::lock_guard lock(state->_mutex); if (!state->_alive) return ULT_ERROR_STATE; + for (std::uint32_t index = 0; index < capacity; ++index) { + if (waitingThreads[index].size != sizeof(UltWaitingThreadInfo)) return ULT_ERROR_INVALID; + } + if (capacity != 0 && state->_waiters != 0) NotImplemented_nid_no_patch(__func__); + if (state->_writing && state->_ownerIdentity.type != 1) NotImplemented_nid_no_patch(__func__); std::memset(info, 0, sizeof(*info)); info->size = sizeof(UltReaderWriterLockInfo); info->object = object; @@ -217,12 +227,9 @@ int APS5_VABI sceUltGetReaderWriterLockInfo(void* object, UltReaderWriterLockInf info->waitingQueuePool = state->_waitingPool; info->readers = state->_readers; info->waitingThreads = state->_waiters; - info->lockType = state->_writing ? 1 : (state->_readers > 0 ? 2 : 0); - const auto toCopy = std::min(capacity, info->waitingThreads); - for (std::uint32_t i = 0; i < toCopy; ++i) { - std::memset(&waitingThreads[i], 0, sizeof(UltWaitingThreadInfo)); - waitingThreads[i].size = sizeof(UltWaitingThreadInfo); - } + info->lockType = state->_writing ? 2 : (state->_readers > 0 ? 1 : 0); + info->owner = state->_ownerIdentity.thread; + info->ownerType = state->_ownerIdentity.type; return ULT_OK; } diff --git a/core/libs/prx/libSceUlt/src/ReaderWriterLock.hpp b/core/libs/prx/libSceUlt/src/ReaderWriterLock.hpp index df98dcd077..b8c0b7d704 100644 --- a/core/libs/prx/libSceUlt/src/ReaderWriterLock.hpp +++ b/core/libs/prx/libSceUlt/src/ReaderWriterLock.hpp @@ -1,6 +1,9 @@ #ifndef CORE_LIBS_PRX_LIBSCEULT_SRC_READERWRITERLOCK_HPP #define CORE_LIBS_PRX_LIBSCEULT_SRC_READERWRITERLOCK_HPP +struct UltThreadIdentity; + +UltThreadIdentity UltCurrentThreadIdentity_nid_no_patch(); bool UltWaitingPoolExists(void* pool); bool UltReaderWriterLocksWaitOn(void* pool); void UltFinalizeReaderWriterLocks(); diff --git a/core/libs/tests/GuestUltSync.cpp b/core/libs/tests/GuestUltSync.cpp index 4cb19d52b9..f8e54452d0 100644 --- a/core/libs/tests/GuestUltSync.cpp +++ b/core/libs/tests/GuestUltSync.cpp @@ -1,6 +1,6 @@ #include "prx/libSceUlt/include/Api.hpp" +#include "prx/libkernel/include/Api.hpp" #include "SceTypes.hpp" -#include "SceUltTypes.hpp" #include #include #include @@ -271,6 +271,60 @@ void Ulthreads() { CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == Ok); } +void* CurrentThreadId() { + std::uintptr_t id; + const auto self = scePthreadSelf(); + std::memcpy(&id, self, sizeof(id)); + return reinterpret_cast(id); +} + +template +void CheckUnsupportedInfo(TQuery query, void* object, std::uint32_t capacity = 4) { + TInfo info; + std::memset(&info, 0xEE, sizeof(info)); + UltWaitingThreadInfo waiters[4]; + std::memset(waiters, 0xAA, sizeof(waiters)); + for (auto& waiter : waiters) waiter.size = sizeof(UltWaitingThreadInfo); + std::uint8_t savedWaiters[sizeof(waiters)]; + std::memcpy(savedWaiters, waiters, sizeof(waiters)); + bool threw = false; + try { + query(object, &info, waiters, capacity); + } catch (const std::runtime_error&) { + threw = true; + } + CHECK(threw); + const auto* bytes = reinterpret_cast(&info); + for (std::size_t index = 0; index < sizeof(info); ++index) CHECK(bytes[index] == 0xEE); + CHECK(std::memcmp(waiters, savedWaiters, sizeof(waiters)) == 0); +} + +template +void CheckWaitingRecordSizes(TQuery query, void* object) { + TInfo info; + UltWaitingThreadInfo waiters[4]; + for (std::size_t invalid = 0; invalid < 4; ++invalid) { + for (const std::uint64_t size : {std::uint64_t{0}, std::uint64_t{63}, std::uint64_t{65}, UINT64_MAX}) { + std::memset(&info, 0xEE, sizeof(info)); + std::memset(waiters, 0xAA, sizeof(waiters)); + for (auto& waiter : waiters) waiter.size = sizeof(UltWaitingThreadInfo); + waiters[invalid].size = size; + std::uint8_t savedWaiters[sizeof(waiters)]; + std::memcpy(savedWaiters, waiters, sizeof(waiters)); + CHECK(query(object, &info, waiters, 4) == Invalid); + const auto* bytes = reinterpret_cast(&info); + for (std::size_t index = 0; index < sizeof(info); ++index) CHECK(bytes[index] == 0xEE); + CHECK(std::memcmp(waiters, savedWaiters, sizeof(waiters)) == 0); + } + } + CHECK(query(object, &info, waiters, 0) == Ok); + for (auto& waiter : waiters) waiter.size = sizeof(UltWaitingThreadInfo); + std::uint8_t savedWaiters[sizeof(waiters)]; + std::memcpy(savedWaiters, waiters, sizeof(waiters)); + CHECK(query(object, &info, waiters, 4) == Ok); + CHECK(std::memcmp(waiters, savedWaiters, sizeof(waiters)) == 0); +} + void GetInfoTests() { alignas(8) std::uint8_t dummy[512]{}; alignas(8) std::uint8_t poolObj[256]{}; @@ -292,6 +346,7 @@ void GetInfoTests() { UltUlthreadRuntimeInfo rtInfo{}; UltUlthreadInfo threadInfo{}; UltWaitingThreadInfo waitThreads[4]{}; + for (auto& waiter : waitThreads) waiter.size = sizeof(UltWaitingThreadInfo); CHECK(sceUltGetMutexInfo(nullptr, nullptr, nullptr, 0) == Null); CHECK(sceUltGetMutexInfo(mutexObj, nullptr, nullptr, 0) == Null); @@ -334,11 +389,32 @@ void GetInfoTests() { CHECK(mutexInfo.waitingQueuePool == poolObj); CHECK(mutexInfo.lockCount == 0); CHECK(mutexInfo.waitingThreads == 0); + CHECK(mutexInfo.owner == nullptr && mutexInfo.ownerType == 0); + CheckWaitingRecordSizes(sceUltGetMutexInfo, mutexObj); CHECK(sceUltMutexLock(mutexObj) == Ok); CHECK(sceUltGetMutexInfo(mutexObj, &mutexInfo, nullptr, 0) == Ok); - CHECK(mutexInfo.lockCount == 1); + CHECK(mutexInfo.lockCount == 0); + const auto owner = CurrentThreadId(); + CHECK(owner != nullptr && mutexInfo.owner == owner && mutexInfo.ownerType == 1); + std::thread([&] { + CHECK(CurrentThreadId() != owner); + UltMutexInfo held{}; + CHECK(sceUltGetMutexInfo(mutexObj, &held, nullptr, 0) == Ok); + CHECK(held.owner == owner && held.ownerType == 1 && held.lockCount == 0); + }).join(); CHECK(sceUltMutexUnlock(mutexObj) == Ok); + CHECK(sceUltGetMutexInfo(mutexObj, &mutexInfo, nullptr, 0) == Ok); + CHECK(mutexInfo.owner == nullptr && mutexInfo.ownerType == 0 && mutexInfo.lockCount == 0); + + Object recursiveMutex{}; + UltMutexOptParam recursiveOptions{}; + recursiveOptions.attribute = 1; + CHECK(sceUltMutexCreate(&recursiveMutex, "recursive-info", poolObj, &recursiveOptions, 0) == Ok); + CHECK(sceUltMutexLock(&recursiveMutex) == Ok); + CheckUnsupportedInfo(sceUltGetMutexInfo, &recursiveMutex, 0); + CHECK(sceUltMutexUnlock(&recursiveMutex) == Ok); + CHECK(sceUltMutexDestroy(&recursiveMutex) == Ok); CHECK(sceUltConditionVariableCreate(condObj, "test-cnd", mutexObj, nullptr, 0) == Ok); CHECK(sceUltGetConditionVariableInfo(condObj, &condInfo, waitThreads, 4) == Ok); @@ -364,12 +440,32 @@ void GetInfoTests() { CHECK(rwInfo.waitingQueuePool == poolObj); CHECK(rwInfo.readers == 0); CHECK(rwInfo.lockType == 0); + CHECK(rwInfo.owner == nullptr && rwInfo.ownerType == 0); + CheckWaitingRecordSizes(sceUltGetReaderWriterLockInfo, rwObj); CHECK(sceUltReaderWriterLockLockRead(rwObj) == Ok); CHECK(sceUltGetReaderWriterLockInfo(rwObj, &rwInfo, nullptr, 0) == Ok); CHECK(rwInfo.readers == 1); - CHECK(rwInfo.lockType == 2); + CHECK(rwInfo.lockType == 1); + CHECK(rwInfo.owner == nullptr && rwInfo.ownerType == 0); + CHECK(sceUltReaderWriterLockLockRead(rwObj) == Ok); + CHECK(sceUltGetReaderWriterLockInfo(rwObj, &rwInfo, nullptr, 0) == Ok); + CHECK(rwInfo.lockType == 1 && rwInfo.readers == 2 && rwInfo.owner == nullptr && rwInfo.ownerType == 0); CHECK(sceUltReaderWriterLockUnlockRead(rwObj) == Ok); + CHECK(sceUltReaderWriterLockUnlockRead(rwObj) == Ok); + CHECK(sceUltGetReaderWriterLockInfo(rwObj, &rwInfo, nullptr, 0) == Ok); + CHECK(rwInfo.lockType == 0 && rwInfo.readers == 0 && rwInfo.owner == nullptr && rwInfo.ownerType == 0); + for (const auto writeLock : {sceUltReaderWriterLockLockWrite, sceUltReaderWriterLockTryLockWrite}) { + CHECK(writeLock(rwObj) == Ok); + std::thread([&] { + UltReaderWriterLockInfo held{}; + CHECK(sceUltGetReaderWriterLockInfo(rwObj, &held, nullptr, 0) == Ok); + CHECK(held.lockType == 2 && held.readers == 0 && held.owner == owner && held.ownerType == 1); + }).join(); + CHECK(sceUltReaderWriterLockUnlockWrite(rwObj) == Ok); + CHECK(sceUltGetReaderWriterLockInfo(rwObj, &rwInfo, nullptr, 0) == Ok); + CHECK(rwInfo.lockType == 0 && rwInfo.owner == nullptr && rwInfo.ownerType == 0); + } CHECK(sceUltQueueDataResourcePoolCreate(dataPoolObj, "test-datapool", 8, 64, 2, poolObj, nullptr, nullptr, 0) == Ok); CHECK(sceUltGetQueueDataResourcePoolInfo(dataPoolObj, &dataPoolInfo) == Ok); @@ -465,14 +561,11 @@ void QueueWaiters() { WaitForQueueWaiters(queueObj, &info, 0, 1); CHECK(info.queuedItems == 0); std::memset(waiters, 0xAA, sizeof(waiters)); - CHECK(sceUltGetQueueInfo(queueObj, &info, waiters, 4) == Ok); - CHECK(info.waitingPushers == 0); - CHECK(info.waitingPoppers == 1); - CHECK(waiters[0].size == sizeof(UltWaitingThreadInfo)); - for (std::size_t i = 1; i < sizeof(waiters) / sizeof(waiters[0]); ++i) { - const auto* bytes = reinterpret_cast(&waiters[i]); - for (std::size_t j = 0; j < sizeof(UltWaitingThreadInfo); ++j) CHECK(bytes[j] == 0xAA); - } + CheckUnsupportedInfo(sceUltGetQueueInfo, queueObj, 1); + CheckUnsupportedInfo(sceUltGetQueueInfo, queueObj); + CHECK(sceUltGetQueueInfo(queueObj, &info, waiters, 0) == Ok); + const auto* waiterBytes = reinterpret_cast(waiters); + for (std::size_t index = 0; index < sizeof(waiters); ++index) CHECK(waiterBytes[index] == 0xAA); CHECK(sceUltQueueDestroy(queueObj) == State); CHECK(sceUltQueuePush(queueObj, item) == Ok); popper.join(); @@ -487,6 +580,8 @@ void QueueWaiters() { std::thread pusher([&] { pushResult.store(sceUltQueuePush(queueObj, item)); }); WaitForQueueWaiters(queueObj, &info, 1, 0); CHECK(info.queuedItems == 2); + CheckUnsupportedInfo(sceUltGetQueueInfo, queueObj, 1); + CheckUnsupportedInfo(sceUltGetQueueInfo, queueObj); CHECK(sceUltGetQueueInfo(queueObj, &info, waiters, 0) == Ok); CHECK(info.waitingPushers == 1); CHECK(info.waitingPoppers == 0); @@ -503,6 +598,79 @@ void QueueWaiters() { CHECK(sceUltWaitingQueueResourcePoolDestroy(poolObj) == Ok); } +template +void WaitForInfoWaiters(TQuery query, void* object) { + for (int attempt = 0; attempt < 4000; ++attempt) { + TInfo info{}; + CHECK(query(object, &info, nullptr, 0) == Ok); + if (info.waitingThreads == 1) return; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + CHECK(false); +} + +void InfoWaiters() { + Object pool{}; + Object mutex{}; + Object condition{}; + Object semaphore{}; + Object rwlock{}; + CHECK(sceUltWaitingQueueResourcePoolCreate(&pool, "info-pool", 4, 4, nullptr, nullptr, 0) == Ok); + CHECK(sceUltMutexCreate(&mutex, "info-mutex", &pool, nullptr, 0) == Ok); + CHECK(sceUltConditionVariableCreate(&condition, "info-condition", &mutex, nullptr, 0) == Ok); + CHECK(sceUltSemaphoreCreate(&semaphore, "info-semaphore", 0, &pool, nullptr, 0) == Ok); + CHECK(sceUltReaderWriterLockCreate(&rwlock, "info-rwlock", &pool, nullptr, 0) == Ok); + + CHECK(sceUltMutexLock(&mutex) == Ok); + std::thread mutexWaiter([&] { + CHECK(sceUltMutexLock(&mutex) == Ok); + CHECK(sceUltMutexUnlock(&mutex) == Ok); + }); + WaitForInfoWaiters(sceUltGetMutexInfo, &mutex); + CheckUnsupportedInfo(sceUltGetMutexInfo, &mutex, 1); + CheckUnsupportedInfo(sceUltGetMutexInfo, &mutex); + CHECK(sceUltMutexUnlock(&mutex) == Ok); + mutexWaiter.join(); + + std::thread conditionWaiter([&] { + CHECK(sceUltMutexLock(&mutex) == Ok); + CHECK(sceUltConditionVariableWait(&condition) == Ok); + UltMutexInfo info{}; + CHECK(sceUltGetMutexInfo(&mutex, &info, nullptr, 0) == Ok); + CHECK(info.owner == CurrentThreadId() && info.ownerType == 1); + CHECK(sceUltMutexUnlock(&mutex) == Ok); + }); + WaitForInfoWaiters(sceUltGetConditionVariableInfo, &condition); + CheckUnsupportedInfo(sceUltGetConditionVariableInfo, &condition, 1); + CheckUnsupportedInfo(sceUltGetConditionVariableInfo, &condition); + CHECK(sceUltConditionVariableSignal(&condition) == Ok); + conditionWaiter.join(); + + std::thread semaphoreWaiter([&] { CHECK(sceUltSemaphoreAcquire(&semaphore, 1) == Ok); }); + WaitForInfoWaiters(sceUltGetSemaphoreInfo, &semaphore); + CheckUnsupportedInfo(sceUltGetSemaphoreInfo, &semaphore, 1); + CheckUnsupportedInfo(sceUltGetSemaphoreInfo, &semaphore); + CHECK(sceUltSemaphoreRelease(&semaphore, 1) == Ok); + semaphoreWaiter.join(); + + CHECK(sceUltReaderWriterLockLockWrite(&rwlock) == Ok); + std::thread reader([&] { + CHECK(sceUltReaderWriterLockLockRead(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockUnlockRead(&rwlock) == Ok); + }); + WaitForInfoWaiters(sceUltGetReaderWriterLockInfo, &rwlock); + CheckUnsupportedInfo(sceUltGetReaderWriterLockInfo, &rwlock, 1); + CheckUnsupportedInfo(sceUltGetReaderWriterLockInfo, &rwlock); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == Ok); + reader.join(); + + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Ok); + CHECK(sceUltSemaphoreDestroy(&semaphore) == Ok); + CHECK(sceUltConditionVariableDestroy(&condition) == Ok); + CHECK(sceUltMutexDestroy(&mutex) == Ok); + CHECK(sceUltWaitingQueueResourcePoolDestroy(&pool) == Ok); +} + } int main(int argc, char** argv) { @@ -518,6 +686,7 @@ int main(int argc, char** argv) { else if (std::strcmp(argv[1], "ulthreads") == 0) Ulthreads(); else if (std::strcmp(argv[1], "get-info") == 0) GetInfoTests(); else if (std::strcmp(argv[1], "queue-waiters") == 0) QueueWaiters(); + else if (std::strcmp(argv[1], "info-waiters") == 0) InfoWaiters(); else return 2; CHECK(sceUltFinalize() == Ok); std::puts("ULT sync tests passed"); diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index c1f5bdb962..c9e0118fab 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -7,7 +7,8 @@ - [libSceUlt ulthreads](../../../core/libs/prx/libSceUlt/Export.cpp) - Runtime option defaults still omit the native header, stack size, worker affinity and priority fields; affinity depends on unresolved process configuration queries. Concurrent join ownership is not modeled: the native runtime distinguishes an ordinary running thread from one with another joiner. Host threads do not use the guest context or worker scheduler. - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - Queue-data pool destruction and shared slot limits still require comparison with the native node allocator. - [`sceUltConditionVariableCreate` and `sceUltConditionVariableOptParamInitialize`](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) are absent from the console export list, where only the underscored variants `_sceUltConditionVariableCreate` and `_sceUltConditionVariableOptParamInitialize` appear; their non-underscored signatures are assumed. Condition variable option parameter initialization only zeroes 128 bytes, and its layout without the common header remains unverified on hardware. -- [Status query exports](../../../core/libs/prx/libSceUlt/Export.cpp) (`sceUltGetConditionVariableInfo`, `sceUltGetMutexInfo`, `sceUltGetQueueDataResourcePoolInfo`, `sceUltGetQueueInfo`, `sceUltGetReaderWriterLockInfo`, `sceUltGetSemaphoreInfo`, `sceUltGetUlthreadInfo`, `sceUltGetUlthreadRuntimeInfo` and `sceUltGetWaitingQueueResourcePoolInfo`) populate recovered structure layouts and return `ULT_ERROR_NULL` or `ULT_ERROR_STATE` on absent objects. `sceUltGetQueueInfo` counts blocked pushers and poppers in their separate fields, but the waiter records it copies only carry their size: host waiters are not enumerated, so no thread identity, wait type or wait value is reported. `sceUltGetUlthreadRuntimeInfo` throws when the caller supplies a non-empty worker thread array, because this runtime starts no worker threads and has no handles to return. Waiting thread arrays and hardware priority accounting remain unverified on PS5 hardware. +- [Status query exports](../../../core/libs/prx/libSceUlt/Export.cpp) lack waiter identities, types and wait values. All five synchronization queries throw before writing output when capacity is nonzero and waiters exist; capacity-zero queries still report counts. `sceUltGetUlthreadRuntimeInfo` throws for a non-empty worker array because no worker threads are started. Recovered layouts, waiting records, worker scheduling and priority accounting lack full console validation. +- [Mutex and reader-writer status queries](../../../core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp) throw for held recursive mutexes and ulthread-owned mutexes or write locks: recursive counts and ulthread owner encoding remain unverified. [Firmware 13.42 observations](https://github.com/boykopovar/AnyPS5/pull/9975#issuecomment-6417685562), from native x86-64 module runs with stubbed imports rather than PS5 hardware, supply `lockType` 1/2/0 for read/write/free, pthread `owner` from the thread block's first 8 bytes, `ownerType` 1 and default mutex `lockCount` 0 when held once. Both queries reject any supplied waiting record whose `size` is not 64 with `ULT_ERROR_INVALID` before writing output, as observed without waiters. Validation precedence and record-size checks for the other queries remain unverified. - [Ult factory and creation exports](../../../core/libs/prx/libSceUlt/Export.cpp) without a leading underscore are absent from the supplied module; their signatures remain unresolved. Diagnostic signatures and synchronization argument lists are recovered. - [sceUltMutexTryLock, the reader-writer lock and sceUltUlthreadGetSelf, sceUltUlthreadYield and sceUltUlthreadExit](../../../core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp) (libSceUlt) - the signatures follow the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.cpp) (a 256-byte lock and 128-byte option structures), not a PS5 binary. The rules are assumed: readers may take the lock while a writer waits, a thread that holds the write lock gets `ULT_ERROR_STATE` for any further lock instead of waiting on itself, a reader asking for the write lock waits on its own read lock, and the try forms return `ULT_ERROR_BUSY`. Outside an ulthread, `sceUltUlthreadGetSelf`, `sceUltUlthreadYield` and `sceUltUlthreadExit` return `ULT_ERROR_STATE`; inside one they act on the guest thread it runs on, since ulthreads are not scheduled cooperatively here