diff --git a/core/libs/prx/libSceUlt/CMakeLists.txt b/core/libs/prx/libSceUlt/CMakeLists.txt index 56c1216ca..e9ce2a507 100644 --- a/core/libs/prx/libSceUlt/CMakeLists.txt +++ b/core/libs/prx/libSceUlt/CMakeLists.txt @@ -15,4 +15,17 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + + add_test_executable(guest_ult_thread_tests ${LIBS_INCLUDE_DIR}/tests/GuestUltThread.cpp) + 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) + 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) + set_tests_properties(guest_ult_thread_${testCase} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() endif() diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index a224d3c35..979033e5a 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -123,6 +123,9 @@ int APS5_VABI sceUltFinalize() { std::vector> semaphores; { std::lock_guard lock(gMutex); + if (!gUlthreads.empty()) { + return ULT_ERROR_BUSY; + } semaphores.reserve(gSemaphores.size()); for (auto& entry : gSemaphores) { auto& state = entry.second; @@ -143,7 +146,6 @@ int APS5_VABI sceUltFinalize() { gQueues.clear(); gQueueDataPools.clear(); gRuntimes.clear(); - gUlthreads.clear(); } for (const auto& state : semaphores) { state->_available.notify_all(); diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp new file mode 100644 index 000000000..e46308a40 --- /dev/null +++ b/core/libs/tests/GuestUltThread.cpp @@ -0,0 +1,94 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceUltInitialize(); +int APS5_VABI sceUltFinalize(); +std::uint64_t APS5_VABI sceUltUlthreadRuntimeGetWorkAreaSize(std::uint32_t, 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); +int APS5_VABI sceUltUlthreadJoin(void*, std::int32_t*); +} + +static constexpr int Ok = 0; +static constexpr int Null = -2139029503; +static constexpr int State = -2139029498; +static constexpr int Busy = -2139029497; + +static void Require(bool value, int line) { + if (!value) { + std::fprintf(stderr, "ULT thread check failed at line %d\n", line); + std::abort(); + } +} + +#define CHECK(value) Require((value), __LINE__) + +static std::atomic gStarted{false}; +static std::atomic gRelease{false}; + +static std::int32_t APS5_VABI Parked(std::uint64_t arg) { + gStarted.store(true, std::memory_order_release); + while (!gRelease.load(std::memory_order_acquire)) std::this_thread::yield(); + return static_cast(arg); +} + +static std::int32_t APS5_VABI Immediate(std::uint64_t arg) { + return static_cast(arg); +} + +struct alignas(8) Runtime { std::uint8_t bytes[4096]{}; }; +struct alignas(8) Thread { std::uint8_t bytes[512]{}; }; + +static void Outstanding() { + Runtime runtime; + Thread thread; + CHECK(sceUltUlthreadRuntimeCreate(&runtime, "runtime", 1, 1, nullptr, nullptr, 0) == Ok); + CHECK(sceUltUlthreadCreate(&thread, "parked", Parked, 0x5A, nullptr, 0, &runtime, nullptr, 0) == Ok); + while (!gStarted.load(std::memory_order_acquire)) std::this_thread::yield(); + + const int outstanding = sceUltFinalize(); + gRelease.store(true, std::memory_order_release); + std::int32_t status = 0; + const int joined = sceUltUlthreadJoin(&thread, &status); + + CHECK(outstanding == Busy); + CHECK(joined == Ok && status == 0x5A); + CHECK(sceUltUlthreadJoin(&thread, &status) == State); + CHECK(sceUltFinalize() == Ok); +} + +static void Validation() { + Runtime runtime; + Thread thread; + gStarted.store(false, std::memory_order_release); + gRelease.store(false, std::memory_order_release); + CHECK(sceUltUlthreadRuntimeCreate(nullptr, "runtime", 1, 1, nullptr, nullptr, 0) == Null); + CHECK(sceUltUlthreadRuntimeCreate(&runtime, "runtime", 1, 1, nullptr, nullptr, 0) == Ok); + CHECK(sceUltUlthreadCreate(nullptr, "thread", Immediate, 0, nullptr, 0, &runtime, nullptr, 0) == Null); + CHECK(sceUltUlthreadCreate(&thread, "thread", nullptr, 0, nullptr, 0, &runtime, nullptr, 0) == Null); + CHECK(sceUltUlthreadCreate(&thread, "thread", Immediate, 0, nullptr, 0, nullptr, nullptr, 0) == Null); + CHECK(sceUltUlthreadJoin(nullptr, nullptr) == Null); + CHECK(sceUltUlthreadJoin(&thread, nullptr) == State); + + CHECK(sceUltUlthreadCreate(&thread, "thread", Immediate, 0x27, nullptr, 0, &runtime, nullptr, 0) == Ok); + CHECK(sceUltUlthreadCreate(&thread, "thread", Immediate, 0x27, nullptr, 0, &runtime, nullptr, 0) == State); + CHECK(sceUltFinalize() == Busy); + std::int32_t status = 0; + CHECK(sceUltUlthreadJoin(&thread, &status) == Ok && status == 0x27); + CHECK(sceUltFinalize() == Ok); + CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(1, 1) == 256u + 16u * 1024u); +} + +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 CHECK(false); +}