diff --git a/core/libs/prx/libSceUlt/CMakeLists.txt b/core/libs/prx/libSceUlt/CMakeLists.txt index e9ce2a507..c9005a3cb 100644 --- a/core/libs/prx/libSceUlt/CMakeLists.txt +++ b/core/libs/prx/libSceUlt/CMakeLists.txt @@ -20,7 +20,7 @@ if(BUILD_TESTING) target_include_directories(guest_ult_thread_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_ult_thread_tests PRIVATE libSceUlt libc) configure_windows_unwind(guest_ult_thread_tests) - foreach(testCase IN ITEMS outstanding validation) + foreach(testCase IN ITEMS outstanding validation tryjoin destroy) add_test(NAME guest_ult_thread_${testCase} COMMAND guest_ult_thread_tests ${testCase}) set_tests_properties(guest_ult_thread_${testCase} PROPERTIES TIMEOUT 20) if(WIN32) diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index 979033e5a..d87bd7e5f 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -108,7 +108,9 @@ void ReleaseQueue(UltQueueState& state) { void* APS5_VABI ulthreadRunner(void* arg) { auto* state = static_cast(arg); - return reinterpret_cast(static_cast(state->_entry(state->_arg))); + const auto result = state->_entry(state->_arg); + state->_exited.store(true, std::memory_order_release); + return reinterpret_cast(static_cast(result)); } } @@ -489,6 +491,7 @@ int APS5_VABI sceUltUlthreadCreate(void* ulthread, const char* name, UltUlthread auto state = std::make_shared(); state->_entry = entry; state->_arg = arg; + state->_runtime = runtime; { std::lock_guard lock(gMutex); if (gRuntimes.find(runtime) == gRuntimes.end()) { @@ -534,4 +537,51 @@ int APS5_VABI sceUltUlthreadJoin(void* ulthread, std::int32_t* status) { return ULT_OK; } +int APS5_VABI sceUltUlthreadTryJoin(void* ulthread, std::int32_t* status) { + if (ulthread == nullptr) { + return ULT_ERROR_NULL; + } + { + std::lock_guard lock(gMutex); + auto it = gUlthreads.find(ulthread); + if (it == gUlthreads.end()) { + return ULT_ERROR_STATE; + } + if (!it->second->_exited.load(std::memory_order_acquire)) { + return ULT_ERROR_BUSY; + } + } + return sceUltUlthreadJoin(ulthread, status); +} + +int APS5_VABI sceUltUlthreadRuntimeDestroy(void* runtime) { + if (runtime == nullptr) { + return ULT_ERROR_NULL; + } + std::lock_guard lock(gMutex); + auto it = gRuntimes.find(runtime); + if (it == gRuntimes.end()) { + return ULT_ERROR_STATE; + } + for (const auto& entry : gUlthreads) { + if (entry.second->_runtime == runtime) { + return ULT_ERROR_BUSY; + } + } + gRuntimes.erase(it); + return ULT_OK; +} + +int APS5_VABI _sceUltUlthreadRuntimeOptParamInitialize(UltUlthreadRuntimeOptParam* optParam, std::uint32_t buildVersion) { + return sceUltUlthreadRuntimeOptParamInitialize(optParam, buildVersion); +} + +int APS5_VABI _sceUltUlthreadRuntimeCreate(void* runtime, const char* name, std::uint32_t maxNumUlthread, std::uint32_t numWorkerThread, void* workArea, const void* optParam, std::uint32_t buildVersion) { + return sceUltUlthreadRuntimeCreate(runtime, name, maxNumUlthread, numWorkerThread, workArea, optParam, buildVersion); +} + +int APS5_VABI _sceUltUlthreadCreate(void* ulthread, const char* name, UltUlthreadEntry entry, std::uint64_t arg, void* context, std::uint64_t sizeContext, void* runtime, const void* optParam, std::uint32_t buildVersion) { + return sceUltUlthreadCreate(ulthread, name, entry, arg, context, sizeContext, runtime, optParam, buildVersion); +} + } diff --git a/core/libs/prx/libSceUlt/SceUltTypes.hpp b/core/libs/prx/libSceUlt/SceUltTypes.hpp index 1a8a0c8d5..0a314a9d5 100644 --- a/core/libs/prx/libSceUlt/SceUltTypes.hpp +++ b/core/libs/prx/libSceUlt/SceUltTypes.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBSCEULT_SCEULTTYPES_HPP #define CORE_LIBS_PRX_LIBSCEULT_SCEULTTYPES_HPP +#include #include #include #include @@ -70,6 +71,8 @@ struct UltUlthreadState { UltUlthreadEntry _entry = nullptr; std::uint64_t _arg = 0; Pthread _thread = nullptr; + void* _runtime = nullptr; + std::atomic _exited{false}; }; #endif diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp index e46308a40..13ac01a59 100644 --- a/core/libs/tests/GuestUltThread.cpp +++ b/core/libs/tests/GuestUltThread.cpp @@ -13,6 +13,11 @@ std::uint64_t APS5_VABI sceUltUlthreadRuntimeGetWorkAreaSize(std::uint32_t, std: int APS5_VABI sceUltUlthreadRuntimeCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*, std::uint32_t); int APS5_VABI sceUltUlthreadCreate(void*, const char*, UltUlthreadEntry, std::uint64_t, void*, std::uint64_t, void*, const void*, std::uint32_t); int APS5_VABI sceUltUlthreadJoin(void*, std::int32_t*); +int APS5_VABI sceUltUlthreadTryJoin(void*, std::int32_t*); +int APS5_VABI sceUltUlthreadRuntimeDestroy(void*); +int APS5_VABI _sceUltUlthreadRuntimeOptParamInitialize(void*, std::uint32_t); +int APS5_VABI _sceUltUlthreadRuntimeCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*, std::uint32_t); +int APS5_VABI _sceUltUlthreadCreate(void*, const char*, UltUlthreadEntry, std::uint64_t, void*, std::uint64_t, void*, const void*, std::uint32_t); } static constexpr int Ok = 0; @@ -85,10 +90,55 @@ static void Validation() { CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(1, 1) == 256u + 16u * 1024u); } +static void TryJoin() { + Runtime runtime; + Thread thread; + std::uint8_t optParam[128]; + std::memset(optParam, 0xff, sizeof(optParam)); + CHECK(_sceUltUlthreadRuntimeOptParamInitialize(nullptr, 0) == Null); + CHECK(_sceUltUlthreadRuntimeOptParamInitialize(optParam, 0) == Ok && optParam[0] == 0); + CHECK(_sceUltUlthreadRuntimeCreate(&runtime, "runtime", 1, 1, nullptr, nullptr, 0) == Ok); + CHECK(_sceUltUlthreadCreate(&thread, "parked", Parked, 0x3C, nullptr, 0, &runtime, nullptr, 0) == Ok); + while (!gStarted.load(std::memory_order_acquire)) std::this_thread::yield(); + + std::int32_t status = 0; + const int running = sceUltUlthreadTryJoin(&thread, &status); + gRelease.store(true, std::memory_order_release); + int joined; + while ((joined = sceUltUlthreadTryJoin(&thread, &status)) == Busy) std::this_thread::yield(); + + CHECK(running == Busy); + CHECK(joined == Ok && status == 0x3C); + CHECK(sceUltUlthreadTryJoin(&thread, &status) == State); + CHECK(sceUltUlthreadTryJoin(nullptr, &status) == Null); + CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == Ok); +} + +static void Destroy() { + Runtime runtime; + Runtime other; + Thread thread; + CHECK(sceUltUlthreadRuntimeDestroy(nullptr) == Null); + CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == State); + CHECK(_sceUltUlthreadRuntimeCreate(&runtime, "runtime", 1, 1, nullptr, nullptr, 0) == Ok); + CHECK(_sceUltUlthreadRuntimeCreate(&other, "other", 1, 1, nullptr, nullptr, 0) == Ok); + CHECK(_sceUltUlthreadCreate(&thread, "thread", Immediate, 0x11, nullptr, 0, &runtime, nullptr, 0) == Ok); + CHECK(sceUltUlthreadRuntimeDestroy(&other) == Ok); + CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == Busy); + std::int32_t status = 0; + CHECK(sceUltUlthreadJoin(&thread, &status) == Ok && status == 0x11); + CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == Ok); + CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == State); + CHECK(_sceUltUlthreadCreate(&thread, "thread", Immediate, 0, nullptr, 0, &runtime, nullptr, 0) == State); + CHECK(sceUltFinalize() == Ok); +} + int main(int argc, char** argv) { CHECK(argc == 2); CHECK(sceUltInitialize() == Ok); if (std::strcmp(argv[1], "outstanding") == 0) Outstanding(); else if (std::strcmp(argv[1], "validation") == 0) Validation(); + else if (std::strcmp(argv[1], "tryjoin") == 0) TryJoin(); + else if (std::strcmp(argv[1], "destroy") == 0) Destroy(); else CHECK(false); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 6631c12fe..4c4fb5864 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -219,6 +219,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceKernelAddTimerEvent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - `usec` is taken as the period in microseconds; re-adding an existing timer keeps its pending expiration count and restarts the period from the time of the call, where FreeBSD 12+ `filt_timertouch` clears pending expirations on `EV_ADD`; `usec == 0` fires on every wait. None of this is confirmed on a PS5 title - [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - only shadPS4 (PS4) has bodies. A null tick returns `INVALID_POINTER` like the existing `sceRtcFormatRFC3339`, where shadPS4 formats the current time instead; offsets outside ±1439 minutes return `INVALID_VALUE` like `sceRtcFormatRFC3339`; the local-time variants use the host's offset at the given instant (shadPS4 uses `sceKernelGettimezone`, the current offset) - [snwprintf_s](../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: `set_constraint_handler_s` is not exported and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` +- [libSceUlt ulthreads](../../core/libs/prx/libSceUlt/Export.cpp) - `_sceUltUlthreadRuntimeOptParamInitialize`, `_sceUltUlthreadRuntimeCreate` and `_sceUltUlthreadCreate` are assumed to take the same arguments as the existing exports without the underscore, including the trailing build version. `sceUltUlthreadTryJoin` is assumed to return `ULT_ERROR_BUSY` while the ulthread runs, and `sceUltUlthreadRuntimeDestroy` to return `ULT_ERROR_BUSY` while an ulthread created on the runtime is not joined. Neither error has been confirmed on PS5 hardware - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. Non-null queue and data-pool option parameters throw because their layouts are unknown. - [LinuxProtFromSce](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - protection bits 0x100 and 0x200 are accepted and assumed not to affect host CPU access; host protection is derived only from the read, write and execute bits - [_sceUlobjmgrRegisterObject](../../core/libs/prx/ulobjmgr/Export.cpp) (ulobjmgr) - parameters taken from shadPS4 (PS4): a nonzero 64-bit object, a nonzero 32-bit kind and a 32-bit id output; the meaning of the first two and the PS5 signature are unverified. The id bound of `_sceUlobjmgrUnregisterObject` (below 0x4000) and the raw `EINVAL` (22) return also come from shadPS4