feat(libSceUlt): implement sceUltUlthreadTryJoin, sceUltUlthreadRuntimeDestroy and the underscore ulthread exports

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
EdouardSence 2026-10-09 12:05:45 +02:00 committed by Adrià Franch
parent 54f28f2d31
commit 04dda44d3d
5 changed files with 106 additions and 2 deletions

View File

@ -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)

View File

@ -108,7 +108,9 @@ void ReleaseQueue(UltQueueState& state) {
void* APS5_VABI ulthreadRunner(void* arg) {
auto* state = static_cast<UltUlthreadState*>(arg);
return reinterpret_cast<void*>(static_cast<std::intptr_t>(state->_entry(state->_arg)));
const auto result = state->_entry(state->_arg);
state->_exited.store(true, std::memory_order_release);
return reinterpret_cast<void*>(static_cast<std::intptr_t>(result));
}
}
@ -489,6 +491,7 @@ int APS5_VABI sceUltUlthreadCreate(void* ulthread, const char* name, UltUlthread
auto state = std::make_shared<UltUlthreadState>();
state->_entry = entry;
state->_arg = arg;
state->_runtime = runtime;
{
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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);
}
}

View File

@ -1,6 +1,7 @@
#ifndef CORE_LIBS_PRX_LIBSCEULT_SCEULTTYPES_HPP
#define CORE_LIBS_PRX_LIBSCEULT_SCEULTTYPES_HPP
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <memory>
@ -70,6 +71,8 @@ struct UltUlthreadState {
UltUlthreadEntry _entry = nullptr;
std::uint64_t _arg = 0;
Pthread _thread = nullptr;
void* _runtime = nullptr;
std::atomic<bool> _exited{false};
};
#endif

View File

@ -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);
}

View File

@ -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