From ac0f8af2289d533b06b064068039a0f7437a2103 Mon Sep 17 00:00:00 2001 From: QDarkRex Date: Sun, 27 Sep 2026 13:22:01 +0700 Subject: [PATCH 01/46] fix(kernel): use SCE kernel error codes in equeue, direct memory and mutex SCE kernel errors are 0x80020000 plus the FreeBSD errno. The equeue and direct memory constants used an unrelated 0x8001xxxx range with mismatched errno values (e.g. ETIMEDOUT was 0x80010023), so a guest checking sceKernelWaitEqueue for SCE_KERNEL_ERROR_ETIMEDOUT saw an unexpected error. SCE_KERNEL_ERROR_EDEADLK in Mutex.cpp held the EAGAIN value. Co-Authored-By: Claude Opus 5.5 --- core/libs/CMakeLists.txt | 10 ++++ .../libkernel/DirectMemory/DirectMemory.hpp | 10 ++-- core/libs/prx/libkernel/Equeue/Equeue.hpp | 10 ++-- core/libs/prx/libkernel/Pthread/src/Mutex.cpp | 2 +- core/libs/tests/GuestKernelErrors.cpp | 56 +++++++++++++++++++ 5 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 core/libs/tests/GuestKernelErrors.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index c8abb5269..0b60019d2 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -269,6 +269,16 @@ if(BUILD_TESTING) set_tests_properties(guest_signals PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_executable(guest_kernel_error_tests tests/GuestKernelErrors.cpp) + target_include_directories(guest_kernel_error_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_error_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_kernel_error_tests) + add_test(NAME guest_kernel_errors COMMAND guest_kernel_error_tests) + set_tests_properties(guest_kernel_errors PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_kernel_errors PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_system_configuration_tests tests/GuestSystemConfiguration.cpp) target_include_directories(guest_system_configuration_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_system_configuration_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index c53b17f05..ff97aaeb1 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -7,11 +7,11 @@ static constexpr size_t DIRECT_MEMORY_SIZE = 13824ULL * 1024 * 1024; static constexpr size_t PS5_PAGE_SIZE = 0x4000; -static constexpr int SCE_KERNEL_ERROR_EINVAL = -2147418107; -static constexpr int SCE_KERNEL_ERROR_EAGAIN = -2147418110; -static constexpr int SCE_KERNEL_ERROR_ENOMEM = -2147418105; -static constexpr int SCE_KERNEL_ERROR_EACCES = -2147418108; -static constexpr int SCE_KERNEL_ERROR_EFAULT = -2147418103; +static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; +static constexpr int SCE_KERNEL_ERROR_EAGAIN = 0x80020023; +static constexpr int SCE_KERNEL_ERROR_ENOMEM = 0x8002000C; +static constexpr int SCE_KERNEL_ERROR_EACCES = 0x8002000D; +static constexpr int SCE_KERNEL_ERROR_EFAULT = 0x8002000E; int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int64_t* physOut); void DirectMemoryFree(int64_t start, size_t len); diff --git a/core/libs/prx/libkernel/Equeue/Equeue.hpp b/core/libs/prx/libkernel/Equeue/Equeue.hpp index 0082a1edd..e8571a78e 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.hpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.hpp @@ -23,11 +23,11 @@ static constexpr uint16_t EV_CLEAR = 0x0020; static constexpr uint16_t EV_ERROR = 0x4000; static constexpr int EQUEUE_OK = 0; -static constexpr int EQUEUE_ERROR_EBADF = -2147418090; -static constexpr int EQUEUE_ERROR_EFAULT = -2147418103; -static constexpr int EQUEUE_ERROR_EINVAL = -2147418107; -static constexpr int EQUEUE_ERROR_ENOENT = -2147418095; -static constexpr int EQUEUE_ERROR_ETIMEDOUT = -2147418077; +static constexpr int EQUEUE_ERROR_EBADF = 0x80020009; +static constexpr int EQUEUE_ERROR_EFAULT = 0x8002000E; +static constexpr int EQUEUE_ERROR_EINVAL = 0x80020016; +static constexpr int EQUEUE_ERROR_ENOENT = 0x80020002; +static constexpr int EQUEUE_ERROR_ETIMEDOUT = 0x8002003C; struct KernelEqueueEvent; diff --git a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp index e586e4060..2f216474c 100644 --- a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp @@ -6,7 +6,7 @@ static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_ENOMEM = 0x8002000C; -static constexpr int SCE_KERNEL_ERROR_EDEADLK = 0x80020023; +static constexpr int SCE_KERNEL_ERROR_EDEADLK = 0x8002000B; static constexpr int SCE_KERNEL_ERROR_EPERM = 0x80020001; extern "C" { diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp new file mode 100644 index 000000000..96997af26 --- /dev/null +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -0,0 +1,56 @@ +#include "SceTypes.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name); +int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq); +int APS5_VABI sceKernelWaitEqueue(KernelEqueue eq, KernelEvent* ev, int num, int* out, const KernelUseconds* timo); +int APS5_VABI sceKernelDeleteUserEvent(KernelEqueue eq, int id); +int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr); +int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr); +int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type); +int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name); +int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); +} + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_ENOENT = static_cast(0x80020002); +static constexpr int SCE_KERNEL_ERROR_EBADF = static_cast(0x80020009); +static constexpr int SCE_KERNEL_ERROR_EDEADLK = static_cast(0x8002000B); +static constexpr int SCE_KERNEL_ERROR_EFAULT = static_cast(0x8002000E); +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); +static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); +static constexpr int MUTEX_TYPE_ERRORCHECK = 1; + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + KernelEqueue eq = 0; + Require(sceKernelCreateEqueue(&eq, "errors") == SCE_OK); + KernelEvent event{}; + int count = -1; + const KernelUseconds timeout = 1000; + Require(sceKernelWaitEqueue(eq, &event, 1, &count, &timeout) == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(count == 0); + Require(sceKernelWaitEqueue(eq, nullptr, 1, &count, &timeout) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelWaitEqueue(eq, &event, 0, &count, &timeout) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelDeleteUserEvent(eq, 7) == SCE_KERNEL_ERROR_ENOENT); + Require(sceKernelDeleteEqueue(eq) == SCE_OK); + Require(sceKernelDeleteEqueue(eq) == SCE_KERNEL_ERROR_EBADF); + Require(sceKernelWaitEqueue(eq, &event, 1, &count, &timeout) == SCE_KERNEL_ERROR_EBADF); + Require(sceKernelCreateEqueue(nullptr, "errors") == SCE_KERNEL_ERROR_EINVAL); + + PthreadMutexattr attr = nullptr; + Require(scePthreadMutexattrInit(&attr) == SCE_OK); + Require(scePthreadMutexattrSettype(&attr, MUTEX_TYPE_ERRORCHECK) == SCE_OK); + PthreadMutex mutex = nullptr; + Require(scePthreadMutexInit(&mutex, &attr, nullptr) == SCE_OK); + Require(scePthreadMutexattrDestroy(&attr) == SCE_OK); + Require(scePthreadMutexLock(&mutex) == SCE_OK); + Require(scePthreadMutexLock(&mutex) == SCE_KERNEL_ERROR_EDEADLK); + Require(scePthreadMutexUnlock(&mutex) == SCE_OK); + Require(scePthreadMutexDestroy(&mutex) == SCE_OK); +} From 2081071237b59900e10ac66f0fd45417009557b2 Mon Sep 17 00:00:00 2001 From: QDarkRex Date: Sun, 27 Sep 2026 13:32:28 +0700 Subject: [PATCH 02/46] refactor(kernel): share SCE kernel error codes through KernelErrors.hpp Kernel error codes were redeclared per file under three prefixes (SCE_KERNEL_ERROR_, EQUEUE_ERROR_, KERNEL_SEMA_ERROR_), which let wrong values go unnoticed. Define them once as 0x80020000 | errno for errno 1..94 and use the SCE_KERNEL_ERROR_ names everywhere. Values are unchanged. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceVideoOut/src/Event.cpp | 6 +- .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 4 +- .../libkernel/DirectMemory/DirectMemory.hpp | 8 +- core/libs/prx/libkernel/Equeue/Equeue.cpp | 42 +++---- core/libs/prx/libkernel/Equeue/Equeue.hpp | 6 +- core/libs/prx/libkernel/File/src/Open.cpp | 3 +- core/libs/prx/libkernel/KernelErrors.hpp | 103 ++++++++++++++++++ core/libs/prx/libkernel/Pthread/src/Attr.cpp | 2 +- core/libs/prx/libkernel/Pthread/src/Cond.cpp | 3 +- core/libs/prx/libkernel/Pthread/src/Mutex.cpp | 4 +- .../libs/prx/libkernel/Pthread/src/Thread.cpp | 2 +- .../libkernel/Semaphore/include/Semaphore.hpp | 5 +- .../prx/libkernel/Semaphore/src/Semaphore.cpp | 10 +- 13 files changed, 143 insertions(+), 55 deletions(-) create mode 100644 core/libs/prx/libkernel/KernelErrors.hpp diff --git a/core/libs/prx/libSceVideoOut/src/Event.cpp b/core/libs/prx/libSceVideoOut/src/Event.cpp index fcf64b9e9..01b2b7d79 100644 --- a/core/libs/prx/libSceVideoOut/src/Event.cpp +++ b/core/libs/prx/libSceVideoOut/src/Event.cpp @@ -72,7 +72,7 @@ static int registerVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind, e->event.data = 0; }; const int result = EqueueAddEvent_nid_postfix(eq, event); - if (result == EQUEUE_ERROR_EBADF) { + if (result == SCE_KERNEL_ERROR_EBADF) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE"); } if (result != EQUEUE_OK) throw std::runtime_error("VideoOut: event registration failed"); @@ -99,10 +99,10 @@ static int deleteVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind) { events->erase(std::remove_if(events->begin(), events->end(), [eq](const EventRegistration& r) { return r.eq == eq; }), events->end()); } const int result = EqueueDeleteEvent_nid_postfix(eq, static_cast(eventKind), EVFILT_VIDEO_OUT); - if (result == EQUEUE_ERROR_EBADF) { + if (result == SCE_KERNEL_ERROR_EBADF) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE"); } - return (result == EQUEUE_ERROR_ENOENT) ? 0 : result; + return (result == SCE_KERNEL_ERROR_ENOENT) ? 0 : result; } extern "C" { diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index f933db424..9067f586b 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -278,7 +278,7 @@ bool VideoOutDriver::close(int handle) { const auto removeEvents = [](const auto& events, int kind) { for (const auto& event : events) { const auto result = EqueueDeleteEvent_nid_postfix(event.eq, static_cast(kind), EVFILT_VIDEO_OUT); - require(result == EQUEUE_OK || result == EQUEUE_ERROR_EBADF || result == EQUEUE_ERROR_ENOENT, "event removal during close failed"); + require(result == EQUEUE_OK || result == SCE_KERNEL_ERROR_EBADF || result == SCE_KERNEL_ERROR_ENOENT, "event removal during close failed"); } }; removeEvents(cfg->flipEvents, VIDEO_OUT_EVENT_FLIP); @@ -329,7 +329,7 @@ void VideoOutDriver::triggerEvents(VideoOutConfig& cfg, int eventKind, void* tri for (auto it = events->begin(); it != events->end();) { require(it->generation == cfg.generation, "stale event registration"); const auto result = EqueueTriggerEvent_nid_postfix(it->eq, static_cast(eventKind), EVFILT_VIDEO_OUT, triggerData); - if (result == EQUEUE_ERROR_EBADF || result == EQUEUE_ERROR_ENOENT) it = events->erase(it); + if (result == SCE_KERNEL_ERROR_EBADF || result == SCE_KERNEL_ERROR_ENOENT) it = events->erase(it); else { require(result == EQUEUE_OK, "event delivery failed"); ++it; diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index ff97aaeb1..4c7f3f477 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -4,15 +4,11 @@ #include #include +#include "prx/libkernel/KernelErrors.hpp" + static constexpr size_t DIRECT_MEMORY_SIZE = 13824ULL * 1024 * 1024; static constexpr size_t PS5_PAGE_SIZE = 0x4000; -static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; -static constexpr int SCE_KERNEL_ERROR_EAGAIN = 0x80020023; -static constexpr int SCE_KERNEL_ERROR_ENOMEM = 0x8002000C; -static constexpr int SCE_KERNEL_ERROR_EACCES = 0x8002000D; -static constexpr int SCE_KERNEL_ERROR_EFAULT = 0x8002000E; - int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int64_t* physOut); void DirectMemoryFree(int64_t start, size_t len); int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, size_t alignment); diff --git a/core/libs/prx/libkernel/Equeue/Equeue.cpp b/core/libs/prx/libkernel/Equeue/Equeue.cpp index 3b9b49b27..e0d4cc3fa 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -70,7 +70,7 @@ bool KernelEqueuePrivate::NextTimerWaitMicros(uint64_t nowNs, uint32_t* out) con int KernelEqueuePrivate::GetTriggeredEvents(KernelEvent* ev, int num) { std::unique_lock lock(m_mutex); if (m_closed) { - return EQUEUE_ERROR_EBADF; + return SCE_KERNEL_ERROR_EBADF; } TriggerExpiredTimers(MonotonicNs()); int ret = 0; @@ -110,7 +110,7 @@ int KernelEqueuePrivate::GetTriggeredEvents(KernelEvent* ev, int num) { int KernelEqueuePrivate::WaitForEvents(KernelEvent* ev, int num, uint32_t micros) { std::unique_lock lock(m_mutex); if (m_closed) { - return EQUEUE_ERROR_EBADF; + return SCE_KERNEL_ERROR_EBADF; } const auto deadline = std::chrono::steady_clock::now() + std::chrono::microseconds(micros); @@ -148,7 +148,7 @@ int KernelEqueuePrivate::WaitForEvents(KernelEvent* ev, int num, uint32_t micros return ret; } if (m_closed) { - return EQUEUE_ERROR_EBADF; + return SCE_KERNEL_ERROR_EBADF; } if (micros == 0) { m_cond.wait(lock); @@ -173,7 +173,7 @@ int KernelEqueuePrivate::WaitForEvents(KernelEvent* ev, int num, uint32_t micros int KernelEqueuePrivate::AddEvent(const KernelEqueueEvent& event) { std::unique_lock lock(m_mutex); if (m_closed) { - return EQUEUE_ERROR_EBADF; + return SCE_KERNEL_ERROR_EBADF; } auto it = std::find_if(m_events.begin(), m_events.end(), [ident = event.event.ident, filter = event.event.filter](const auto& e) { @@ -196,7 +196,7 @@ int KernelEqueuePrivate::AddEvent(const KernelEqueueEvent& event) { int KernelEqueuePrivate::TriggerEvent(uintptr_t ident, int16_t filter, void* triggerData) { std::unique_lock lock(m_mutex); if (m_closed) { - return EQUEUE_ERROR_EBADF; + return SCE_KERNEL_ERROR_EBADF; } auto it = std::find_if(m_events.begin(), m_events.end(), [ident, filter](const auto& e) { @@ -204,7 +204,7 @@ int KernelEqueuePrivate::TriggerEvent(uintptr_t ident, int16_t filter, void* tri } ); if (it == m_events.end()) { - return EQUEUE_ERROR_ENOENT; + return SCE_KERNEL_ERROR_ENOENT; } if (it->filter.triggerFunc != nullptr) { it->filter.triggerFunc(&*it, triggerData); @@ -218,7 +218,7 @@ int KernelEqueuePrivate::TriggerEvent(uintptr_t ident, int16_t filter, void* tri int KernelEqueuePrivate::DeleteEvent(uintptr_t ident, int16_t filter) { std::unique_lock lock(m_mutex); if (m_closed) { - return EQUEUE_ERROR_EBADF; + return SCE_KERNEL_ERROR_EBADF; } auto it = std::find_if(m_events.begin(), m_events.end(), [ident, filter](const auto& e) { @@ -226,7 +226,7 @@ int KernelEqueuePrivate::DeleteEvent(uintptr_t ident, int16_t filter) { } ); if (it == m_events.end()) { - return EQUEUE_ERROR_ENOENT; + return SCE_KERNEL_ERROR_ENOENT; } if (it->filter.deleteEventFunc != nullptr) { auto owner = it->filter.owner; @@ -248,7 +248,7 @@ KernelEqueueRef EqueuePin_nid_postfix(KernelEqueue eq) { int APS5_VABI EqueueAddEvent_nid_postfix(KernelEqueue eq, const KernelEqueueEvent& event) { auto owner = EqueuePin_nid_postfix(eq); if (!owner) { - return EQUEUE_ERROR_EBADF; + return SCE_KERNEL_ERROR_EBADF; } return owner->AddEvent(event); } @@ -256,7 +256,7 @@ int APS5_VABI EqueueAddEvent_nid_postfix(KernelEqueue eq, const KernelEqueueEven int APS5_VABI EqueueTriggerEvent_nid_postfix(KernelEqueue eq, uintptr_t ident, int16_t filter, void* triggerData) { auto owner = EqueuePin_nid_postfix(eq); if (!owner) { - return EQUEUE_ERROR_EBADF; + return SCE_KERNEL_ERROR_EBADF; } return owner->TriggerEvent(ident, filter, triggerData); } @@ -264,7 +264,7 @@ int APS5_VABI EqueueTriggerEvent_nid_postfix(KernelEqueue eq, uintptr_t ident, i int APS5_VABI EqueueDeleteEvent_nid_postfix(KernelEqueue eq, uintptr_t ident, int16_t filter) { auto owner = EqueuePin_nid_postfix(eq); if (!owner) { - return EQUEUE_ERROR_EBADF; + return SCE_KERNEL_ERROR_EBADF; } return owner->DeleteEvent(ident, filter); } @@ -272,7 +272,7 @@ int APS5_VABI EqueueDeleteEvent_nid_postfix(KernelEqueue eq, uintptr_t ident, in int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name) { if (eq == nullptr || name == nullptr) { - return EQUEUE_ERROR_EINVAL; + return SCE_KERNEL_ERROR_EINVAL; } std::unique_lock lock(g_equeueMutex); if (g_nextEqueue > static_cast(std::numeric_limits::max())) { @@ -291,7 +291,7 @@ int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq) { std::unique_lock lock(g_equeueMutex); auto it = g_equeues.find(eq); if (it == g_equeues.end()) { - return EQUEUE_ERROR_EBADF; + return SCE_KERNEL_ERROR_EBADF; } owner = std::move(it->second); g_equeues.erase(it); @@ -303,13 +303,13 @@ int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq) { int APS5_VABI sceKernelWaitEqueue(KernelEqueue eq, KernelEvent* ev, int num, int* out, const KernelUseconds* timo) { auto owner = EqueuePin_nid_postfix(eq); if (!owner) { - return EQUEUE_ERROR_EBADF; + return SCE_KERNEL_ERROR_EBADF; } if (ev == nullptr) { - return EQUEUE_ERROR_EFAULT; + return SCE_KERNEL_ERROR_EFAULT; } if (num < 1 || out == nullptr) { - return EQUEUE_ERROR_EINVAL; + return SCE_KERNEL_ERROR_EINVAL; } if (timo == nullptr) { *out = owner->WaitForEvents(ev, num, 0); @@ -318,11 +318,11 @@ int APS5_VABI sceKernelWaitEqueue(KernelEqueue eq, KernelEvent* ev, int num, int } else { *out = owner->WaitForEvents(ev, num, *timo); } - if (*out == EQUEUE_ERROR_EBADF) { - return EQUEUE_ERROR_EBADF; + if (*out == SCE_KERNEL_ERROR_EBADF) { + return SCE_KERNEL_ERROR_EBADF; } if (*out == 0) { - return EQUEUE_ERROR_ETIMEDOUT; + return SCE_KERNEL_ERROR_ETIMEDOUT; } return EQUEUE_OK; } @@ -377,10 +377,10 @@ int APS5_VABI sceKernelDeleteUserEvent(KernelEqueue eq, int id) { int APS5_VABI sceKernelAddHRTimerEvent(KernelEqueue eq, int id, const KernelTimespec* ts, void* udata) { if (ts == nullptr) { - return EQUEUE_ERROR_EFAULT; + return SCE_KERNEL_ERROR_EFAULT; } if (ts->tv_sec < 0 || ts->tv_nsec < 0 || ts->tv_nsec >= 1000000000LL) { - return EQUEUE_ERROR_EINVAL; + return SCE_KERNEL_ERROR_EINVAL; } const uint64_t delayNs = static_cast(ts->tv_sec) * 1000000000ULL + diff --git a/core/libs/prx/libkernel/Equeue/Equeue.hpp b/core/libs/prx/libkernel/Equeue/Equeue.hpp index e8571a78e..eed258f92 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.hpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.hpp @@ -12,6 +12,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/KernelErrors.hpp" static constexpr int16_t EVFILT_USER = -11; static constexpr int16_t EVFILT_VIDEO_OUT = -13; @@ -23,11 +24,6 @@ static constexpr uint16_t EV_CLEAR = 0x0020; static constexpr uint16_t EV_ERROR = 0x4000; static constexpr int EQUEUE_OK = 0; -static constexpr int EQUEUE_ERROR_EBADF = 0x80020009; -static constexpr int EQUEUE_ERROR_EFAULT = 0x8002000E; -static constexpr int EQUEUE_ERROR_EINVAL = 0x80020016; -static constexpr int EQUEUE_ERROR_ENOENT = 0x80020002; -static constexpr int EQUEUE_ERROR_ETIMEDOUT = 0x8002003C; struct KernelEqueueEvent; diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index d22122583..c12bce199 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -2,6 +2,7 @@ #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/KernelErrors.hpp" #include "SceTypes.hpp" #include @@ -9,8 +10,6 @@ #include #include -static constexpr int SCE_KERNEL_ERROR_ENOENT = -2147352574; - #ifdef _WIN32 #include #include diff --git a/core/libs/prx/libkernel/KernelErrors.hpp b/core/libs/prx/libkernel/KernelErrors.hpp new file mode 100644 index 000000000..b7d37ba4f --- /dev/null +++ b/core/libs/prx/libkernel/KernelErrors.hpp @@ -0,0 +1,103 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_KERNELERRORS_HPP +#define CORE_LIBS_PRX_LIBKERNEL_KERNELERRORS_HPP + +constexpr int SceKernelError(int errnoValue) { + return static_cast(0x80020000u | static_cast(errnoValue)); +} + +inline constexpr int SCE_KERNEL_ERROR_EPERM = SceKernelError(1); +inline constexpr int SCE_KERNEL_ERROR_ENOENT = SceKernelError(2); +inline constexpr int SCE_KERNEL_ERROR_ESRCH = SceKernelError(3); +inline constexpr int SCE_KERNEL_ERROR_EINTR = SceKernelError(4); +inline constexpr int SCE_KERNEL_ERROR_EIO = SceKernelError(5); +inline constexpr int SCE_KERNEL_ERROR_ENXIO = SceKernelError(6); +inline constexpr int SCE_KERNEL_ERROR_E2BIG = SceKernelError(7); +inline constexpr int SCE_KERNEL_ERROR_ENOEXEC = SceKernelError(8); +inline constexpr int SCE_KERNEL_ERROR_EBADF = SceKernelError(9); +inline constexpr int SCE_KERNEL_ERROR_ECHILD = SceKernelError(10); +inline constexpr int SCE_KERNEL_ERROR_EDEADLK = SceKernelError(11); +inline constexpr int SCE_KERNEL_ERROR_ENOMEM = SceKernelError(12); +inline constexpr int SCE_KERNEL_ERROR_EACCES = SceKernelError(13); +inline constexpr int SCE_KERNEL_ERROR_EFAULT = SceKernelError(14); +inline constexpr int SCE_KERNEL_ERROR_ENOTBLK = SceKernelError(15); +inline constexpr int SCE_KERNEL_ERROR_EBUSY = SceKernelError(16); +inline constexpr int SCE_KERNEL_ERROR_EEXIST = SceKernelError(17); +inline constexpr int SCE_KERNEL_ERROR_EXDEV = SceKernelError(18); +inline constexpr int SCE_KERNEL_ERROR_ENODEV = SceKernelError(19); +inline constexpr int SCE_KERNEL_ERROR_ENOTDIR = SceKernelError(20); +inline constexpr int SCE_KERNEL_ERROR_EISDIR = SceKernelError(21); +inline constexpr int SCE_KERNEL_ERROR_EINVAL = SceKernelError(22); +inline constexpr int SCE_KERNEL_ERROR_ENFILE = SceKernelError(23); +inline constexpr int SCE_KERNEL_ERROR_EMFILE = SceKernelError(24); +inline constexpr int SCE_KERNEL_ERROR_ENOTTY = SceKernelError(25); +inline constexpr int SCE_KERNEL_ERROR_ETXTBSY = SceKernelError(26); +inline constexpr int SCE_KERNEL_ERROR_EFBIG = SceKernelError(27); +inline constexpr int SCE_KERNEL_ERROR_ENOSPC = SceKernelError(28); +inline constexpr int SCE_KERNEL_ERROR_ESPIPE = SceKernelError(29); +inline constexpr int SCE_KERNEL_ERROR_EROFS = SceKernelError(30); +inline constexpr int SCE_KERNEL_ERROR_EMLINK = SceKernelError(31); +inline constexpr int SCE_KERNEL_ERROR_EPIPE = SceKernelError(32); +inline constexpr int SCE_KERNEL_ERROR_EDOM = SceKernelError(33); +inline constexpr int SCE_KERNEL_ERROR_ERANGE = SceKernelError(34); +inline constexpr int SCE_KERNEL_ERROR_EAGAIN = SceKernelError(35); +inline constexpr int SCE_KERNEL_ERROR_EINPROGRESS = SceKernelError(36); +inline constexpr int SCE_KERNEL_ERROR_EALREADY = SceKernelError(37); +inline constexpr int SCE_KERNEL_ERROR_ENOTSOCK = SceKernelError(38); +inline constexpr int SCE_KERNEL_ERROR_EDESTADDRREQ = SceKernelError(39); +inline constexpr int SCE_KERNEL_ERROR_EMSGSIZE = SceKernelError(40); +inline constexpr int SCE_KERNEL_ERROR_EPROTOTYPE = SceKernelError(41); +inline constexpr int SCE_KERNEL_ERROR_ENOPROTOOPT = SceKernelError(42); +inline constexpr int SCE_KERNEL_ERROR_EPROTONOSUPPORT = SceKernelError(43); +inline constexpr int SCE_KERNEL_ERROR_ESOCKTNOSUPPORT = SceKernelError(44); +inline constexpr int SCE_KERNEL_ERROR_EOPNOTSUPP = SceKernelError(45); +inline constexpr int SCE_KERNEL_ERROR_EPFNOSUPPORT = SceKernelError(46); +inline constexpr int SCE_KERNEL_ERROR_EAFNOSUPPORT = SceKernelError(47); +inline constexpr int SCE_KERNEL_ERROR_EADDRINUSE = SceKernelError(48); +inline constexpr int SCE_KERNEL_ERROR_EADDRNOTAVAIL = SceKernelError(49); +inline constexpr int SCE_KERNEL_ERROR_ENETDOWN = SceKernelError(50); +inline constexpr int SCE_KERNEL_ERROR_ENETUNREACH = SceKernelError(51); +inline constexpr int SCE_KERNEL_ERROR_ENETRESET = SceKernelError(52); +inline constexpr int SCE_KERNEL_ERROR_ECONNABORTED = SceKernelError(53); +inline constexpr int SCE_KERNEL_ERROR_ECONNRESET = SceKernelError(54); +inline constexpr int SCE_KERNEL_ERROR_ENOBUFS = SceKernelError(55); +inline constexpr int SCE_KERNEL_ERROR_EISCONN = SceKernelError(56); +inline constexpr int SCE_KERNEL_ERROR_ENOTCONN = SceKernelError(57); +inline constexpr int SCE_KERNEL_ERROR_ESHUTDOWN = SceKernelError(58); +inline constexpr int SCE_KERNEL_ERROR_ETOOMANYREFS = SceKernelError(59); +inline constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = SceKernelError(60); +inline constexpr int SCE_KERNEL_ERROR_ECONNREFUSED = SceKernelError(61); +inline constexpr int SCE_KERNEL_ERROR_ELOOP = SceKernelError(62); +inline constexpr int SCE_KERNEL_ERROR_ENAMETOOLONG = SceKernelError(63); +inline constexpr int SCE_KERNEL_ERROR_EHOSTDOWN = SceKernelError(64); +inline constexpr int SCE_KERNEL_ERROR_EHOSTUNREACH = SceKernelError(65); +inline constexpr int SCE_KERNEL_ERROR_ENOTEMPTY = SceKernelError(66); +inline constexpr int SCE_KERNEL_ERROR_EPROCLIM = SceKernelError(67); +inline constexpr int SCE_KERNEL_ERROR_EUSERS = SceKernelError(68); +inline constexpr int SCE_KERNEL_ERROR_EDQUOT = SceKernelError(69); +inline constexpr int SCE_KERNEL_ERROR_ESTALE = SceKernelError(70); +inline constexpr int SCE_KERNEL_ERROR_EREMOTE = SceKernelError(71); +inline constexpr int SCE_KERNEL_ERROR_EBADRPC = SceKernelError(72); +inline constexpr int SCE_KERNEL_ERROR_ERPCMISMATCH = SceKernelError(73); +inline constexpr int SCE_KERNEL_ERROR_EPROGUNAVAIL = SceKernelError(74); +inline constexpr int SCE_KERNEL_ERROR_EPROGMISMATCH = SceKernelError(75); +inline constexpr int SCE_KERNEL_ERROR_EPROCUNAVAIL = SceKernelError(76); +inline constexpr int SCE_KERNEL_ERROR_ENOLCK = SceKernelError(77); +inline constexpr int SCE_KERNEL_ERROR_ENOSYS = SceKernelError(78); +inline constexpr int SCE_KERNEL_ERROR_EFTYPE = SceKernelError(79); +inline constexpr int SCE_KERNEL_ERROR_EAUTH = SceKernelError(80); +inline constexpr int SCE_KERNEL_ERROR_ENEEDAUTH = SceKernelError(81); +inline constexpr int SCE_KERNEL_ERROR_EIDRM = SceKernelError(82); +inline constexpr int SCE_KERNEL_ERROR_ENOMSG = SceKernelError(83); +inline constexpr int SCE_KERNEL_ERROR_EOVERFLOW = SceKernelError(84); +inline constexpr int SCE_KERNEL_ERROR_ECANCELED = SceKernelError(85); +inline constexpr int SCE_KERNEL_ERROR_EILSEQ = SceKernelError(86); +inline constexpr int SCE_KERNEL_ERROR_ENOATTR = SceKernelError(87); +inline constexpr int SCE_KERNEL_ERROR_EDOOFUS = SceKernelError(88); +inline constexpr int SCE_KERNEL_ERROR_EBADMSG = SceKernelError(89); +inline constexpr int SCE_KERNEL_ERROR_EMULTIHOP = SceKernelError(90); +inline constexpr int SCE_KERNEL_ERROR_ENOLINK = SceKernelError(91); +inline constexpr int SCE_KERNEL_ERROR_EPROTO = SceKernelError(92); +inline constexpr int SCE_KERNEL_ERROR_ENOTCAPABLE = SceKernelError(93); +inline constexpr int SCE_KERNEL_ERROR_ECAPMODE = SceKernelError(94); + +#endif diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index 7f71adb80..89db40356 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -1,9 +1,9 @@ #include "../include/Pthread.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/KernelErrors.hpp" #include static constexpr int SCE_OK = 0; -static constexpr int SCE_KERNEL_ERROR_ENOMEM = 0x8002000C; static constexpr std::size_t DEFAULT_STACK_SIZE = 1u << 20; static constexpr int DETACH_JOINABLE = 0; diff --git a/core/libs/prx/libkernel/Pthread/src/Cond.cpp b/core/libs/prx/libkernel/Pthread/src/Cond.cpp index 8d7494f26..7346428f0 100644 --- a/core/libs/prx/libkernel/Pthread/src/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Cond.cpp @@ -1,11 +1,10 @@ #include "../include/Pthread.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/KernelErrors.hpp" #include #include static constexpr int SCE_OK = 0; -static constexpr int SCE_KERNEL_ERROR_ENOMEM = 0x8002000C; -static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = 0x8002003C; extern "C" { diff --git a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp index 2f216474c..8c0223bbb 100644 --- a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp @@ -1,13 +1,11 @@ #include "../include/Pthread.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/KernelErrors.hpp" #include #include #include static constexpr int SCE_OK = 0; -static constexpr int SCE_KERNEL_ERROR_ENOMEM = 0x8002000C; -static constexpr int SCE_KERNEL_ERROR_EDEADLK = 0x8002000B; -static constexpr int SCE_KERNEL_ERROR_EPERM = 0x80020001; extern "C" { diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index aab144818..578e99dcf 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -1,5 +1,6 @@ #include "../include/Pthread.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/KernelErrors.hpp" #include #include #include @@ -12,7 +13,6 @@ #endif static constexpr int SCE_OK = 0; -static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; static constexpr std::size_t DEFAULT_STACK_SIZE = 1u << 20; static constexpr int DETACH_DETACHED = 1; diff --git a/core/libs/prx/libkernel/Semaphore/include/Semaphore.hpp b/core/libs/prx/libkernel/Semaphore/include/Semaphore.hpp index 2ec36278e..d266a5689 100644 --- a/core/libs/prx/libkernel/Semaphore/include/Semaphore.hpp +++ b/core/libs/prx/libkernel/Semaphore/include/Semaphore.hpp @@ -9,12 +9,9 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/KernelErrors.hpp" constexpr int KERNEL_SEMA_OK = 0; -constexpr int KERNEL_SEMA_ERROR_EINVAL = static_cast(0x80020016); -constexpr int KERNEL_SEMA_ERROR_EACCES = static_cast(0x8002000D); -constexpr int KERNEL_SEMA_ERROR_EBUSY = static_cast(0x80020010); -constexpr int KERNEL_SEMA_ERROR_ETIMEDOUT = static_cast(0x8002003C); struct KernelSemaPrivate { KernelSemaPrivate(std::int32_t initCount, std::int32_t maxCount, std::string name, bool isFifo); diff --git a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp index de7bb4808..a608aeec1 100644 --- a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp +++ b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp @@ -27,7 +27,7 @@ int APS5_VABI sceKernelPollSema(KernelSema sem, int need) { std::lock_guard lock(sem->mutex); if (sem->tokenCount < need) { - return KERNEL_SEMA_ERROR_EBUSY; + return SCE_KERNEL_ERROR_EBUSY; } sem->tokenCount -= need; return KERNEL_SEMA_OK; @@ -40,7 +40,7 @@ int APS5_VABI sceKernelSignalSema(KernelSema sem, int count) { std::lock_guard lock(sem->mutex); if (sem->tokenCount + count > sem->maxCount) { - return KERNEL_SEMA_ERROR_EINVAL; + return SCE_KERNEL_ERROR_EINVAL; } sem->tokenCount += count; sem->condition.notify_all(); @@ -65,7 +65,7 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time) if (time == nullptr) { sem->condition.wait(lock, [&] { return sem->tokenCount >= need || sem->deleted; }); if (sem->deleted) { - return KERNEL_SEMA_ERROR_EACCES; + return SCE_KERNEL_ERROR_EACCES; } sem->tokenCount -= need; return KERNEL_SEMA_OK; @@ -74,10 +74,10 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time) auto timeout = std::chrono::microseconds(*time); bool acquired = sem->condition.wait_for(lock, timeout, [&] { return sem->tokenCount >= need || sem->deleted; }); if (sem->deleted) { - return KERNEL_SEMA_ERROR_EACCES; + return SCE_KERNEL_ERROR_EACCES; } if (!acquired) { - return KERNEL_SEMA_ERROR_ETIMEDOUT; + return SCE_KERNEL_ERROR_ETIMEDOUT; } sem->tokenCount -= need; return KERNEL_SEMA_OK; From 3c6b0af6f7641793c4aec671102857851520d927 Mon Sep 17 00:00:00 2001 From: adriadam10 Date: Sun, 27 Sep 2026 18:21:34 +0200 Subject: [PATCH 03/46] feat(tools): add progress map for system libraries and shader instructions tools/progress.py counts implemented prx functions and RDNA instructions recognized by the shader recompiler, and renders treemaps, badges and an index page. The Progress workflow deploys them to GitHub Pages on every push to main, so nothing generated is committed to the repository. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/progress.yml | 30 + README.md | 10 + tools/README.md | 27 + tools/progress.py | 241 +++++++ tools/rdna_isa.txt | 1169 ++++++++++++++++++++++++++++++++ 5 files changed, 1477 insertions(+) create mode 100644 .github/workflows/progress.yml create mode 100644 tools/README.md create mode 100644 tools/progress.py create mode 100644 tools/rdna_isa.txt diff --git a/.github/workflows/progress.yml b/.github/workflows/progress.yml new file mode 100644 index 000000000..93e9f0600 --- /dev/null +++ b/.github/workflows/progress.yml @@ -0,0 +1,30 @@ +name: Progress + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + progress: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v7 + - run: python3 tools/progress.py out + - uses: actions/upload-pages-artifact@v5 + with: + path: out + - id: deployment + uses: actions/deploy-pages@v5 diff --git a/README.md b/README.md index 2f746fbfd..7f6c47a63 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # About +[![libraries](https://boykopovar.github.io/AnyPS5/badge-libraries.svg)](https://boykopovar.github.io/AnyPS5/) [![shaders](https://boykopovar.github.io/AnyPS5/badge-shaders.svg)](https://boykopovar.github.io/AnyPS5/) + +[![progress map](https://boykopovar.github.io/AnyPS5/progress.svg)](https://boykopovar.github.io/AnyPS5/) + +* System libraries: percentage of the functions known to the project so far (declared in [core/libs/prx](core/libs/prx)), not of every PS5 system function. The total grows as more functions are declared. + Tool for automatic executables porting to Linux and Windows. Includes a [relinker](core/relinker) that converts executable to the target system's native format and implementations of [system prx libraries](core/libs/prx) suitable for dynamic linking. No emulation or separate runtime process. @@ -14,6 +20,10 @@ The [shader recompiler](core/shader/recompiler/Recompiler.cpp) successfully prod The real game reaches the logo, main menu, and [gameplay](https://gist.github.com/user-attachments/assets/81d28e9b-c237-4545-b2ca-720071129816) with audio. +[Progress](https://boykopovar.github.io/AnyPS5/), generated by [tools/progress.py](tools/progress.py) on every push to `main`: +- **System libraries\***: each block is a [prx library](core/libs/prx) sized by its functions; green is implemented, blue still calls `NotImplemented`. The total only includes functions already declared in the project, not every function of the PS5 firmware. +- **GPU shader instructions**: each block is an instruction encoding of the AMD RDNA 1 + RDNA 2 [instruction list](tools/rdna_isa.txt); green is recognized by the [shader recompiler](core/shader/recompiler). + [Technical debt of the project](docs/TechnicalDebt.md), [code style conventions](docs/CONVENTIONS.md) ## Build diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 000000000..5a1481df6 --- /dev/null +++ b/tools/README.md @@ -0,0 +1,27 @@ +# Tools + +## progress.py + +Generates the progress map shown in the main [README](../README.md). + +```sh +python3 tools/progress.py +``` + +Requires only Python 3.9+ with the standard library. It writes: + +- `progress.svg`: treemaps for system libraries and GPU shader instructions +- `badge-libraries.svg`, `badge-shaders.svg`: percentage badges +- `index.html`: the map with per-library and per-encoding tables +- `progress.json`: raw numbers + +The [Progress workflow](../.github/workflows/progress.yml) runs it on every push to `main` and deploys the output to GitHub Pages, so nothing generated is committed to the repository. + +### What is measured + +- **System libraries**: functions defined in [core/libs/prx](../core/libs/prx). A function is implemented when it no longer calls `NotImplemented_nid_no_patch`. The total only includes functions already declared in the project, not every function of the PS5 firmware. +- **GPU shader instructions**: instructions from [rdna_isa.txt](rdna_isa.txt) that appear in `RdnaOpcode` of the [shader recompiler](../core/shader/recompiler/RdnaDecoder). FLAT opcodes also count for their GLOBAL and SCRATCH forms, since the decoder accepts all three segments. Decoded opcodes missing from AMD's public list are not counted. + +## rdna_isa.txt + +The AMD RDNA 1 + RDNA 2 instruction list (name and primary encoding), extracted from the [AMD Machine-Readable GPU ISA Specification](https://gpuopen.com/machine-readable-isa/) (MIT License). diff --git a/tools/progress.py b/tools/progress.py new file mode 100644 index 000000000..d90d7b6af --- /dev/null +++ b/tools/progress.py @@ -0,0 +1,241 @@ +import argparse +import json +import os +import re +from html import escape +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +PRX = ROOT / "core" / "libs" / "prx" +OPCODES = ROOT / "core" / "shader" / "recompiler" / "RdnaDecoder" / "include" / "RdnaDecoder" / "RdnaOpcode.hpp" +ISA = Path(__file__).resolve().parent / "rdna_isa.txt" +SOURCE = f'https://github.com/{os.environ.get("GITHUB_REPOSITORY", "boykopovar/AnyPS5")}/blob/main' +DEFINITION = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\([^;{]*\)\s*(?:noexcept\s*)?\{") +STUB = "NotImplemented_nid_no_patch" +FLAT_SEGMENTS = ("GLOBAL_", "SCRATCH_") +OPCODE_SENTINELS = {"Invalid", "Count", "Unknown", "Unsupported"} +OPCODE_ALIASES = { + "TBufferLoadFormatX": "TBUFFER_LOAD_FORMAT_X", + "TBufferLoadFormatXyzw": "TBUFFER_LOAD_FORMAT_XYZW", + "ExpMrt": "EXP", + "ExpPos": "EXP", + "ExpParam": "EXP", + "VAddcU32": "V_ADD_CO_CI_U32", + "VMadMixloF16": "V_FMA_MIXLO_F16", + "VMadMixhiF16": "V_FMA_MIXHI_F16", +} +PANEL_WIDTH, GAP, MAP_HEIGHT, HEADER = 495, 10, 280, 30 +DONE_COLOR, TODO_COLOR, BORDER, TEXT = "#2ea043", "#1f6feb", "#0d1117", "#ffffff" + + +def body_end(text, start): + depth = 0 + for i in range(start, len(text)): + if text[i] == "{": + depth += 1 + elif text[i] == "}": + depth -= 1 + if depth == 0: + return i + return len(text) + + +def scan_library(path): + done, todo = set(), set() + for source in path.rglob("*.cpp"): + text = source.read_text(errors="ignore") + for match in DEFINITION.finditer(text): + name = match.group(1) + if name.endswith("_nid_no_patch"): + continue + body = text[match.end() - 1:body_end(text, match.end() - 1)] + (todo if STUB in body else done).add(name) + todo -= done + return {"name": path.name, "label": path.name.removeprefix("libSce"), "done": len(done), "todo": len(todo)} + + +def summarize(groups): + done = sum(g["done"] for g in groups) + total = done + sum(g["todo"] for g in groups) + return {"done": done, "total": total, "percent": round(100 * done / total, 2) if total else 0, "groups": groups} + + +def collect_libraries(): + return summarize([scan_library(p) for p in sorted(PRX.iterdir()) if p.is_dir()]) + + +def camel(name): + return "".join(part.capitalize() for part in name.split("_")) + + +def collect_shaders(): + isa = {} + for line in ISA.read_text().splitlines(): + if line and not line.startswith("#"): + name, encoding = line.split() + isa[name] = encoding + by_camel = {camel(name): name for name in isa} + enum = re.search(r"enum class RdnaOpcode[^{]*\{(.*?)\};", OPCODES.read_text(), re.S).group(1) + opcodes = [o for o in re.findall(r"^\s*([A-Z]\w*)\s*[,=]", enum, re.M) if o not in OPCODE_SENTINELS] + supported, extra = set(), [] + for opcode in opcodes: + name = OPCODE_ALIASES.get(opcode) or by_camel.get(opcode) + if name in isa: + supported.add(name) + if name.startswith("FLAT_"): + supported.update(n for n in (s + name.removeprefix("FLAT_") for s in FLAT_SEGMENTS) if n in isa) + else: + extra.append(opcode) + groups = {} + for name, encoding in isa.items(): + group = groups.setdefault(encoding, {"name": encoding, "label": encoding, "done": 0, "todo": 0}) + group["done" if name in supported else "todo"] += 1 + result = summarize(sorted(groups.values(), key=lambda g: g["name"])) + result["extra"] = extra + return result + + +def worst_ratio(row, side): + area = sum(row) + return max(max(side * side * r / area ** 2, area ** 2 / (side * side * r)) for r in row) + + +def place(row, x, y, w, h, rects): + thickness = sum(row) / min(w, h) + offset = 0 + for area in row: + length = area / thickness + if w >= h: + rects.append((x, y + offset, thickness, length)) + else: + rects.append((x + offset, y, length, thickness)) + offset += length + return (x + thickness, y, w - thickness, h) if w >= h else (x, y + thickness, w, h - thickness) + + +def squarify(values, x, y, w, h): + total = sum(values) + areas = [v * w * h / total for v in values] + rects, row = [], [] + while areas: + side = min(w, h) + if not row or worst_ratio(row + [areas[0]], side) <= worst_ratio(row, side): + row.append(areas.pop(0)) + continue + x, y, w, h = place(row, x, y, w, h, rects) + row = [] + if row: + place(row, x, y, w, h, rects) + return rects + + +def cells(count, x, y, w, h): + if not count: + return [] + rows = max(1, min(count, round((count * h / w) ** 0.5))) + result = [] + for r in range(rows): + in_row = count // rows + (r < count % rows) + cw = w / in_row + result += [(x + c * cw, y + r * h / rows, cw, h / rows) for c in range(in_row)] + return result + + +def rect(x, y, w, h, color, stroke=0.5): + return (f'') + + +def text(x, y, value, size): + return (f'{escape(value)}') + + +def treemap(title, data, left): + groups = sorted((g for g in data["groups"] if g["done"] + g["todo"]), key=lambda g: -(g["done"] + g["todo"])) + parts = [text(left + 4, 21, f'{title}: {data["percent"]}% ({data["done"]}/{data["total"]})', 16)] + for group, (x, y, w, h) in zip(groups, squarify([g["done"] + g["todo"] for g in groups], left, HEADER, PANEL_WIDTH, MAP_HEIGHT)): + total = group["done"] + group["todo"] + parts.append(f'{escape(group["name"])}: {group["done"]}/{total} ({100 * group["done"] / total:.0f}%)') + for i, cell in enumerate(cells(total, x + 1, y + 1, w - 2, h - 2)): + parts.append(rect(*cell, DONE_COLOR if i < group["done"] else TODO_COLOR)) + parts.append(f'') + label = group["label"][:int((w - 10) / 7)] + if (label == group["label"] or len(label) >= 3) and h > 22: + parts.append(text(x + 5, y + 16, label, 12)) + parts.append("") + return parts + + +def render(libraries, shaders): + width, height = 2 * PANEL_WIDTH + GAP, HEADER + MAP_HEIGHT + parts = [f'', + rect(0, 0, width, height, BORDER, 0)] + parts += treemap("System libraries*", libraries, 0) + parts += treemap("GPU shader instructions", shaders, PANEL_WIDTH + GAP) + parts.append("") + return "\n".join(parts) + + +def badge(label, data): + percent = data["percent"] + color = "#4c1" if percent >= 90 else "#97ca00" if percent >= 60 else "#dfb317" if percent >= 30 else "#fe7d37" + value = f"{percent}%" + left, right = 10 + 7 * len(label), 10 + 7 * len(value) + width = left + right + return (f'' + f'' + f'' + f'{label}{value}') + + +def table(heading, column, data): + rows = [f"

{heading}

", "", + f""] + for group in sorted(data["groups"], key=lambda g: g["name"].lower()): + total = group["done"] + group["todo"] + percent = f'{100 * group["done"] / total:.0f}%' if total else "-" + rows.append(f'') + rows.append(f'') + rows.append("
{column}ImplementedTotal%
{escape(group["name"])}{group["done"]}{total}{percent}
Total{data["done"]}{data["total"]}{data["percent"]}%
") + return "\n".join(rows) + + +def summary(libraries, shaders): + extra = ", ".join(f"{escape(o)}" for o in shaders["extra"]) + return "\n".join([ + '', + '', + 'AnyPS5 progress', + '', + '', + "

Progress

", + 'progress', + f'

Generated by tools/progress.py on every push to main. ' + 'Raw numbers: progress.json.

', + table("System libraries", "Library", libraries), + "

A function is implemented when it no longer calls NotImplemented_nid_no_patch. " + f'The total only includes functions already declared in core/libs/prx, ' + "not every function exported by the PS5 firmware.

", + table("GPU shader instructions", "Encoding", shaders), + f'

The total is the AMD RDNA 1 + RDNA 2 instruction list (tools/rdna_isa.txt). ' + f'An instruction is implemented when the decoder recognizes it. ' + f"Decoded opcodes not present in AMD's public list are not counted: {extra}.

", + "", + ]) + "\n" + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("output", type=Path) + output = parser.parse_args().output + output.mkdir(parents=True, exist_ok=True) + libraries, shaders = collect_libraries(), collect_shaders() + (output / "progress.json").write_text(json.dumps({"libraries": libraries, "shaders": shaders}, indent=2)) + (output / "badge-libraries.svg").write_text(badge("libraries*", libraries)) + (output / "badge-shaders.svg").write_text(badge("shaders", shaders)) + (output / "progress.svg").write_text(render(libraries, shaders)) + (output / "index.html").write_text(summary(libraries, shaders)) + print(f'libraries {libraries["done"]}/{libraries["total"]} ({libraries["percent"]}%)') + print(f'shaders {shaders["done"]}/{shaders["total"]} ({shaders["percent"]}%)') diff --git a/tools/rdna_isa.txt b/tools/rdna_isa.txt new file mode 100644 index 000000000..1d854a6c3 --- /dev/null +++ b/tools/rdna_isa.txt @@ -0,0 +1,1169 @@ +# AMD RDNA 1 + RDNA 2 instruction list (name, primary encoding). +# Source: AMD Machine-Readable GPU ISA Specification, https://gpuopen.com/machine-readable-isa/ +# Copyright (c) 2026 Advanced Micro Devices, Inc., or its affiliates. MIT License. +BUFFER_ATOMIC_ADD MUBUF +BUFFER_ATOMIC_ADD_X2 MUBUF +BUFFER_ATOMIC_AND MUBUF +BUFFER_ATOMIC_AND_X2 MUBUF +BUFFER_ATOMIC_CMPSWAP MUBUF +BUFFER_ATOMIC_CMPSWAP_X2 MUBUF +BUFFER_ATOMIC_CSUB MUBUF +BUFFER_ATOMIC_DEC MUBUF +BUFFER_ATOMIC_DEC_X2 MUBUF +BUFFER_ATOMIC_FCMPSWAP MUBUF +BUFFER_ATOMIC_FCMPSWAP_X2 MUBUF +BUFFER_ATOMIC_FMAX MUBUF +BUFFER_ATOMIC_FMAX_X2 MUBUF +BUFFER_ATOMIC_FMIN MUBUF +BUFFER_ATOMIC_FMIN_X2 MUBUF +BUFFER_ATOMIC_INC MUBUF +BUFFER_ATOMIC_INC_X2 MUBUF +BUFFER_ATOMIC_OR MUBUF +BUFFER_ATOMIC_OR_X2 MUBUF +BUFFER_ATOMIC_SMAX MUBUF +BUFFER_ATOMIC_SMAX_X2 MUBUF +BUFFER_ATOMIC_SMIN MUBUF +BUFFER_ATOMIC_SMIN_X2 MUBUF +BUFFER_ATOMIC_SUB MUBUF +BUFFER_ATOMIC_SUB_X2 MUBUF +BUFFER_ATOMIC_SWAP MUBUF +BUFFER_ATOMIC_SWAP_X2 MUBUF +BUFFER_ATOMIC_UMAX MUBUF +BUFFER_ATOMIC_UMAX_X2 MUBUF +BUFFER_ATOMIC_UMIN MUBUF +BUFFER_ATOMIC_UMIN_X2 MUBUF +BUFFER_ATOMIC_XOR MUBUF +BUFFER_ATOMIC_XOR_X2 MUBUF +BUFFER_GL0_INV MUBUF +BUFFER_GL1_INV MUBUF +BUFFER_LOAD_DWORD MUBUF +BUFFER_LOAD_DWORDX2 MUBUF +BUFFER_LOAD_DWORDX3 MUBUF +BUFFER_LOAD_DWORDX4 MUBUF +BUFFER_LOAD_FORMAT_D16_HI_X MUBUF +BUFFER_LOAD_FORMAT_D16_X MUBUF +BUFFER_LOAD_FORMAT_D16_XY MUBUF +BUFFER_LOAD_FORMAT_D16_XYZ MUBUF +BUFFER_LOAD_FORMAT_D16_XYZW MUBUF +BUFFER_LOAD_FORMAT_X MUBUF +BUFFER_LOAD_FORMAT_XY MUBUF +BUFFER_LOAD_FORMAT_XYZ MUBUF +BUFFER_LOAD_FORMAT_XYZW MUBUF +BUFFER_LOAD_SBYTE MUBUF +BUFFER_LOAD_SBYTE_D16 MUBUF +BUFFER_LOAD_SBYTE_D16_HI MUBUF +BUFFER_LOAD_SHORT_D16 MUBUF +BUFFER_LOAD_SHORT_D16_HI MUBUF +BUFFER_LOAD_SSHORT MUBUF +BUFFER_LOAD_UBYTE MUBUF +BUFFER_LOAD_UBYTE_D16 MUBUF +BUFFER_LOAD_UBYTE_D16_HI MUBUF +BUFFER_LOAD_USHORT MUBUF +BUFFER_STORE_BYTE MUBUF +BUFFER_STORE_BYTE_D16_HI MUBUF +BUFFER_STORE_DWORD MUBUF +BUFFER_STORE_DWORDX2 MUBUF +BUFFER_STORE_DWORDX3 MUBUF +BUFFER_STORE_DWORDX4 MUBUF +BUFFER_STORE_FORMAT_D16_HI_X MUBUF +BUFFER_STORE_FORMAT_D16_X MUBUF +BUFFER_STORE_FORMAT_D16_XY MUBUF +BUFFER_STORE_FORMAT_D16_XYZ MUBUF +BUFFER_STORE_FORMAT_D16_XYZW MUBUF +BUFFER_STORE_FORMAT_X MUBUF +BUFFER_STORE_FORMAT_XY MUBUF +BUFFER_STORE_FORMAT_XYZ MUBUF +BUFFER_STORE_FORMAT_XYZW MUBUF +BUFFER_STORE_SHORT MUBUF +BUFFER_STORE_SHORT_D16_HI MUBUF +DS_ADD_F32 DS +DS_ADD_RTN_F32 DS +DS_ADD_RTN_U32 DS +DS_ADD_RTN_U64 DS +DS_ADD_U32 DS +DS_ADD_U64 DS +DS_AND_B32 DS +DS_AND_B64 DS +DS_AND_RTN_B32 DS +DS_AND_RTN_B64 DS +DS_APPEND DS +DS_BPERMUTE_B32 DS +DS_CMPST_B32 DS +DS_CMPST_B64 DS +DS_CMPST_F32 DS +DS_CMPST_F64 DS +DS_CMPST_RTN_B32 DS +DS_CMPST_RTN_B64 DS +DS_CMPST_RTN_F32 DS +DS_CMPST_RTN_F64 DS +DS_CONDXCHG32_RTN_B64 DS +DS_CONSUME DS +DS_DEC_RTN_U32 DS +DS_DEC_RTN_U64 DS +DS_DEC_U32 DS +DS_DEC_U64 DS +DS_GWS_BARRIER DS +DS_GWS_INIT DS +DS_GWS_SEMA_BR DS +DS_GWS_SEMA_P DS +DS_GWS_SEMA_RELEASE_ALL DS +DS_GWS_SEMA_V DS +DS_INC_RTN_U32 DS +DS_INC_RTN_U64 DS +DS_INC_U32 DS +DS_INC_U64 DS +DS_MAX_F32 DS +DS_MAX_F64 DS +DS_MAX_I32 DS +DS_MAX_I64 DS +DS_MAX_RTN_F32 DS +DS_MAX_RTN_F64 DS +DS_MAX_RTN_I32 DS +DS_MAX_RTN_I64 DS +DS_MAX_RTN_U32 DS +DS_MAX_RTN_U64 DS +DS_MAX_U32 DS +DS_MAX_U64 DS +DS_MIN_F32 DS +DS_MIN_F64 DS +DS_MIN_I32 DS +DS_MIN_I64 DS +DS_MIN_RTN_F32 DS +DS_MIN_RTN_F64 DS +DS_MIN_RTN_I32 DS +DS_MIN_RTN_I64 DS +DS_MIN_RTN_U32 DS +DS_MIN_RTN_U64 DS +DS_MIN_U32 DS +DS_MIN_U64 DS +DS_MSKOR_B32 DS +DS_MSKOR_B64 DS +DS_MSKOR_RTN_B32 DS +DS_MSKOR_RTN_B64 DS +DS_NOP DS +DS_ORDERED_COUNT DS +DS_OR_B32 DS +DS_OR_B64 DS +DS_OR_RTN_B32 DS +DS_OR_RTN_B64 DS +DS_PERMUTE_B32 DS +DS_READ2ST64_B32 DS +DS_READ2ST64_B64 DS +DS_READ2_B32 DS +DS_READ2_B64 DS +DS_READ_ADDTID_B32 DS +DS_READ_B128 DS +DS_READ_B32 DS +DS_READ_B64 DS +DS_READ_B96 DS +DS_READ_I16 DS +DS_READ_I8 DS +DS_READ_I8_D16 DS +DS_READ_I8_D16_HI DS +DS_READ_U16 DS +DS_READ_U16_D16 DS +DS_READ_U16_D16_HI DS +DS_READ_U8 DS +DS_READ_U8_D16 DS +DS_READ_U8_D16_HI DS +DS_RSUB_RTN_U32 DS +DS_RSUB_RTN_U64 DS +DS_RSUB_U32 DS +DS_RSUB_U64 DS +DS_SUB_RTN_U32 DS +DS_SUB_RTN_U64 DS +DS_SUB_U32 DS +DS_SUB_U64 DS +DS_SWIZZLE_B32 DS +DS_WRAP_RTN_B32 DS +DS_WRITE2ST64_B32 DS +DS_WRITE2ST64_B64 DS +DS_WRITE2_B32 DS +DS_WRITE2_B64 DS +DS_WRITE_ADDTID_B32 DS +DS_WRITE_B128 DS +DS_WRITE_B16 DS +DS_WRITE_B16_D16_HI DS +DS_WRITE_B32 DS +DS_WRITE_B64 DS +DS_WRITE_B8 DS +DS_WRITE_B8_D16_HI DS +DS_WRITE_B96 DS +DS_WRXCHG2ST64_RTN_B32 DS +DS_WRXCHG2ST64_RTN_B64 DS +DS_WRXCHG2_RTN_B32 DS +DS_WRXCHG2_RTN_B64 DS +DS_WRXCHG_RTN_B32 DS +DS_WRXCHG_RTN_B64 DS +DS_XOR_B32 DS +DS_XOR_B64 DS +DS_XOR_RTN_B32 DS +DS_XOR_RTN_B64 DS +EXP EXP +FLAT_ATOMIC_ADD FLAT +FLAT_ATOMIC_ADD_X2 FLAT +FLAT_ATOMIC_AND FLAT +FLAT_ATOMIC_AND_X2 FLAT +FLAT_ATOMIC_CMPSWAP FLAT +FLAT_ATOMIC_CMPSWAP_X2 FLAT +FLAT_ATOMIC_DEC FLAT +FLAT_ATOMIC_DEC_X2 FLAT +FLAT_ATOMIC_FCMPSWAP FLAT +FLAT_ATOMIC_FCMPSWAP_X2 FLAT +FLAT_ATOMIC_FMAX FLAT +FLAT_ATOMIC_FMAX_X2 FLAT +FLAT_ATOMIC_FMIN FLAT +FLAT_ATOMIC_FMIN_X2 FLAT +FLAT_ATOMIC_INC FLAT +FLAT_ATOMIC_INC_X2 FLAT +FLAT_ATOMIC_OR FLAT +FLAT_ATOMIC_OR_X2 FLAT +FLAT_ATOMIC_SMAX FLAT +FLAT_ATOMIC_SMAX_X2 FLAT +FLAT_ATOMIC_SMIN FLAT +FLAT_ATOMIC_SMIN_X2 FLAT +FLAT_ATOMIC_SUB FLAT +FLAT_ATOMIC_SUB_X2 FLAT +FLAT_ATOMIC_SWAP FLAT +FLAT_ATOMIC_SWAP_X2 FLAT +FLAT_ATOMIC_UMAX FLAT +FLAT_ATOMIC_UMAX_X2 FLAT +FLAT_ATOMIC_UMIN FLAT +FLAT_ATOMIC_UMIN_X2 FLAT +FLAT_ATOMIC_XOR FLAT +FLAT_ATOMIC_XOR_X2 FLAT +FLAT_LOAD_DWORD FLAT +FLAT_LOAD_DWORDX2 FLAT +FLAT_LOAD_DWORDX3 FLAT +FLAT_LOAD_DWORDX4 FLAT +FLAT_LOAD_SBYTE FLAT +FLAT_LOAD_SBYTE_D16 FLAT +FLAT_LOAD_SBYTE_D16_HI FLAT +FLAT_LOAD_SHORT_D16 FLAT +FLAT_LOAD_SHORT_D16_HI FLAT +FLAT_LOAD_SSHORT FLAT +FLAT_LOAD_UBYTE FLAT +FLAT_LOAD_UBYTE_D16 FLAT +FLAT_LOAD_UBYTE_D16_HI FLAT +FLAT_LOAD_USHORT FLAT +FLAT_STORE_BYTE FLAT +FLAT_STORE_BYTE_D16_HI FLAT +FLAT_STORE_DWORD FLAT +FLAT_STORE_DWORDX2 FLAT +FLAT_STORE_DWORDX3 FLAT +FLAT_STORE_DWORDX4 FLAT +FLAT_STORE_SHORT FLAT +FLAT_STORE_SHORT_D16_HI FLAT +GLOBAL_ATOMIC_ADD GLOBAL +GLOBAL_ATOMIC_ADD_X2 GLOBAL +GLOBAL_ATOMIC_AND GLOBAL +GLOBAL_ATOMIC_AND_X2 GLOBAL +GLOBAL_ATOMIC_CMPSWAP GLOBAL +GLOBAL_ATOMIC_CMPSWAP_X2 GLOBAL +GLOBAL_ATOMIC_CSUB GLOBAL +GLOBAL_ATOMIC_DEC GLOBAL +GLOBAL_ATOMIC_DEC_X2 GLOBAL +GLOBAL_ATOMIC_FCMPSWAP GLOBAL +GLOBAL_ATOMIC_FCMPSWAP_X2 GLOBAL +GLOBAL_ATOMIC_FMAX GLOBAL +GLOBAL_ATOMIC_FMAX_X2 GLOBAL +GLOBAL_ATOMIC_FMIN GLOBAL +GLOBAL_ATOMIC_FMIN_X2 GLOBAL +GLOBAL_ATOMIC_INC GLOBAL +GLOBAL_ATOMIC_INC_X2 GLOBAL +GLOBAL_ATOMIC_OR GLOBAL +GLOBAL_ATOMIC_OR_X2 GLOBAL +GLOBAL_ATOMIC_SMAX GLOBAL +GLOBAL_ATOMIC_SMAX_X2 GLOBAL +GLOBAL_ATOMIC_SMIN GLOBAL +GLOBAL_ATOMIC_SMIN_X2 GLOBAL +GLOBAL_ATOMIC_SUB GLOBAL +GLOBAL_ATOMIC_SUB_X2 GLOBAL +GLOBAL_ATOMIC_SWAP GLOBAL +GLOBAL_ATOMIC_SWAP_X2 GLOBAL +GLOBAL_ATOMIC_UMAX GLOBAL +GLOBAL_ATOMIC_UMAX_X2 GLOBAL +GLOBAL_ATOMIC_UMIN GLOBAL +GLOBAL_ATOMIC_UMIN_X2 GLOBAL +GLOBAL_ATOMIC_XOR GLOBAL +GLOBAL_ATOMIC_XOR_X2 GLOBAL +GLOBAL_LOAD_DWORD GLOBAL +GLOBAL_LOAD_DWORDX2 GLOBAL +GLOBAL_LOAD_DWORDX3 GLOBAL +GLOBAL_LOAD_DWORDX4 GLOBAL +GLOBAL_LOAD_DWORD_ADDTID GLOBAL +GLOBAL_LOAD_SBYTE GLOBAL +GLOBAL_LOAD_SBYTE_D16 GLOBAL +GLOBAL_LOAD_SBYTE_D16_HI GLOBAL +GLOBAL_LOAD_SHORT_D16 GLOBAL +GLOBAL_LOAD_SHORT_D16_HI GLOBAL +GLOBAL_LOAD_SSHORT GLOBAL +GLOBAL_LOAD_UBYTE GLOBAL +GLOBAL_LOAD_UBYTE_D16 GLOBAL +GLOBAL_LOAD_UBYTE_D16_HI GLOBAL +GLOBAL_LOAD_USHORT GLOBAL +GLOBAL_STORE_BYTE GLOBAL +GLOBAL_STORE_BYTE_D16_HI GLOBAL +GLOBAL_STORE_DWORD GLOBAL +GLOBAL_STORE_DWORDX2 GLOBAL +GLOBAL_STORE_DWORDX3 GLOBAL +GLOBAL_STORE_DWORDX4 GLOBAL +GLOBAL_STORE_DWORD_ADDTID GLOBAL +GLOBAL_STORE_SHORT GLOBAL +GLOBAL_STORE_SHORT_D16_HI GLOBAL +IMAGE_ATOMIC_ADD MIMG +IMAGE_ATOMIC_AND MIMG +IMAGE_ATOMIC_CMPSWAP MIMG +IMAGE_ATOMIC_DEC MIMG +IMAGE_ATOMIC_FCMPSWAP MIMG +IMAGE_ATOMIC_FMAX MIMG +IMAGE_ATOMIC_FMIN MIMG +IMAGE_ATOMIC_INC MIMG +IMAGE_ATOMIC_OR MIMG +IMAGE_ATOMIC_SMAX MIMG +IMAGE_ATOMIC_SMIN MIMG +IMAGE_ATOMIC_SUB MIMG +IMAGE_ATOMIC_SWAP MIMG +IMAGE_ATOMIC_UMAX MIMG +IMAGE_ATOMIC_UMIN MIMG +IMAGE_ATOMIC_XOR MIMG +IMAGE_BVH64_INTERSECT_RAY MIMG +IMAGE_BVH_INTERSECT_RAY MIMG +IMAGE_GATHER4 MIMG +IMAGE_GATHER4H MIMG +IMAGE_GATHER4H_PCK MIMG +IMAGE_GATHER4_B MIMG +IMAGE_GATHER4_B_CL MIMG +IMAGE_GATHER4_B_CL_O MIMG +IMAGE_GATHER4_B_O MIMG +IMAGE_GATHER4_C MIMG +IMAGE_GATHER4_CL MIMG +IMAGE_GATHER4_CL_O MIMG +IMAGE_GATHER4_C_B MIMG +IMAGE_GATHER4_C_B_CL MIMG +IMAGE_GATHER4_C_B_CL_O MIMG +IMAGE_GATHER4_C_B_O MIMG +IMAGE_GATHER4_C_CL MIMG +IMAGE_GATHER4_C_CL_O MIMG +IMAGE_GATHER4_C_L MIMG +IMAGE_GATHER4_C_LZ MIMG +IMAGE_GATHER4_C_LZ_O MIMG +IMAGE_GATHER4_C_L_O MIMG +IMAGE_GATHER4_C_O MIMG +IMAGE_GATHER4_L MIMG +IMAGE_GATHER4_LZ MIMG +IMAGE_GATHER4_LZ_O MIMG +IMAGE_GATHER4_L_O MIMG +IMAGE_GATHER4_O MIMG +IMAGE_GATHER8H_PCK MIMG +IMAGE_GET_LOD MIMG +IMAGE_GET_RESINFO MIMG +IMAGE_LOAD MIMG +IMAGE_LOAD_BY2 MIMG +IMAGE_LOAD_BY4 MIMG +IMAGE_LOAD_MIP MIMG +IMAGE_LOAD_MIP_BY2 MIMG +IMAGE_LOAD_MIP_BY4 MIMG +IMAGE_LOAD_MIP_PCK MIMG +IMAGE_LOAD_MIP_PCK2 MIMG +IMAGE_LOAD_MIP_PCK4 MIMG +IMAGE_LOAD_MIP_PCK_SGN MIMG +IMAGE_LOAD_PCK MIMG +IMAGE_LOAD_PCK2 MIMG +IMAGE_LOAD_PCK4 MIMG +IMAGE_LOAD_PCK_SGN MIMG +IMAGE_MSAA_LOAD MIMG +IMAGE_SAMPLE MIMG +IMAGE_SAMPLE_B MIMG +IMAGE_SAMPLE_B_CL MIMG +IMAGE_SAMPLE_B_CL_O MIMG +IMAGE_SAMPLE_B_O MIMG +IMAGE_SAMPLE_C MIMG +IMAGE_SAMPLE_CD MIMG +IMAGE_SAMPLE_CD_CL MIMG +IMAGE_SAMPLE_CD_CL_G16 MIMG +IMAGE_SAMPLE_CD_CL_O MIMG +IMAGE_SAMPLE_CD_CL_O_G16 MIMG +IMAGE_SAMPLE_CD_G16 MIMG +IMAGE_SAMPLE_CD_O MIMG +IMAGE_SAMPLE_CD_O_G16 MIMG +IMAGE_SAMPLE_CL MIMG +IMAGE_SAMPLE_CL_O MIMG +IMAGE_SAMPLE_C_B MIMG +IMAGE_SAMPLE_C_B_CL MIMG +IMAGE_SAMPLE_C_B_CL_O MIMG +IMAGE_SAMPLE_C_B_O MIMG +IMAGE_SAMPLE_C_CD MIMG +IMAGE_SAMPLE_C_CD_CL MIMG +IMAGE_SAMPLE_C_CD_CL_G16 MIMG +IMAGE_SAMPLE_C_CD_CL_O MIMG +IMAGE_SAMPLE_C_CD_CL_O_G16 MIMG +IMAGE_SAMPLE_C_CD_G16 MIMG +IMAGE_SAMPLE_C_CD_O MIMG +IMAGE_SAMPLE_C_CD_O_G16 MIMG +IMAGE_SAMPLE_C_CL MIMG +IMAGE_SAMPLE_C_CL_O MIMG +IMAGE_SAMPLE_C_D MIMG +IMAGE_SAMPLE_C_D_CL MIMG +IMAGE_SAMPLE_C_D_CL_G16 MIMG +IMAGE_SAMPLE_C_D_CL_O MIMG +IMAGE_SAMPLE_C_D_CL_O_G16 MIMG +IMAGE_SAMPLE_C_D_G16 MIMG +IMAGE_SAMPLE_C_D_O MIMG +IMAGE_SAMPLE_C_D_O_G16 MIMG +IMAGE_SAMPLE_C_L MIMG +IMAGE_SAMPLE_C_LZ MIMG +IMAGE_SAMPLE_C_LZ_O MIMG +IMAGE_SAMPLE_C_L_O MIMG +IMAGE_SAMPLE_C_O MIMG +IMAGE_SAMPLE_D MIMG +IMAGE_SAMPLE_D_CL MIMG +IMAGE_SAMPLE_D_CL_G16 MIMG +IMAGE_SAMPLE_D_CL_O MIMG +IMAGE_SAMPLE_D_CL_O_G16 MIMG +IMAGE_SAMPLE_D_G16 MIMG +IMAGE_SAMPLE_D_O MIMG +IMAGE_SAMPLE_D_O_G16 MIMG +IMAGE_SAMPLE_L MIMG +IMAGE_SAMPLE_LZ MIMG +IMAGE_SAMPLE_LZ_O MIMG +IMAGE_SAMPLE_L_O MIMG +IMAGE_SAMPLE_O MIMG +IMAGE_STORE MIMG +IMAGE_STORE_BY2 MIMG +IMAGE_STORE_BY4 MIMG +IMAGE_STORE_MIP MIMG +IMAGE_STORE_MIP_BY2 MIMG +IMAGE_STORE_MIP_BY4 MIMG +IMAGE_STORE_MIP_PCK MIMG +IMAGE_STORE_MIP_PCK2 MIMG +IMAGE_STORE_MIP_PCK4 MIMG +IMAGE_STORE_PCK MIMG +IMAGE_STORE_PCK2 MIMG +IMAGE_STORE_PCK4 MIMG +SCRATCH_LOAD_DWORD SCRATCH +SCRATCH_LOAD_DWORDX2 SCRATCH +SCRATCH_LOAD_DWORDX3 SCRATCH +SCRATCH_LOAD_DWORDX4 SCRATCH +SCRATCH_LOAD_SBYTE SCRATCH +SCRATCH_LOAD_SBYTE_D16 SCRATCH +SCRATCH_LOAD_SBYTE_D16_HI SCRATCH +SCRATCH_LOAD_SHORT_D16 SCRATCH +SCRATCH_LOAD_SHORT_D16_HI SCRATCH +SCRATCH_LOAD_SSHORT SCRATCH +SCRATCH_LOAD_UBYTE SCRATCH +SCRATCH_LOAD_UBYTE_D16 SCRATCH +SCRATCH_LOAD_UBYTE_D16_HI SCRATCH +SCRATCH_LOAD_USHORT SCRATCH +SCRATCH_STORE_BYTE SCRATCH +SCRATCH_STORE_BYTE_D16_HI SCRATCH +SCRATCH_STORE_DWORD SCRATCH +SCRATCH_STORE_DWORDX2 SCRATCH +SCRATCH_STORE_DWORDX3 SCRATCH +SCRATCH_STORE_DWORDX4 SCRATCH +SCRATCH_STORE_SHORT SCRATCH +SCRATCH_STORE_SHORT_D16_HI SCRATCH +S_ABSDIFF_I32 SOP2 +S_ABS_I32 SOP1 +S_ADDC_U32 SOP2 +S_ADDK_I32 SOPK +S_ADD_I32 SOP2 +S_ADD_U32 SOP2 +S_ANDN1_SAVEEXEC_B32 SOP1 +S_ANDN1_SAVEEXEC_B64 SOP1 +S_ANDN1_WREXEC_B32 SOP1 +S_ANDN1_WREXEC_B64 SOP1 +S_ANDN2_B32 SOP2 +S_ANDN2_B64 SOP2 +S_ANDN2_SAVEEXEC_B32 SOP1 +S_ANDN2_SAVEEXEC_B64 SOP1 +S_ANDN2_WREXEC_B32 SOP1 +S_ANDN2_WREXEC_B64 SOP1 +S_AND_B32 SOP2 +S_AND_B64 SOP2 +S_AND_SAVEEXEC_B32 SOP1 +S_AND_SAVEEXEC_B64 SOP1 +S_ASHR_I32 SOP2 +S_ASHR_I64 SOP2 +S_ATC_PROBE SMEM +S_ATC_PROBE_BUFFER SMEM +S_BARRIER SOPP +S_BCNT0_I32_B32 SOP1 +S_BCNT0_I32_B64 SOP1 +S_BCNT1_I32_B32 SOP1 +S_BCNT1_I32_B64 SOP1 +S_BFE_I32 SOP2 +S_BFE_I64 SOP2 +S_BFE_U32 SOP2 +S_BFE_U64 SOP2 +S_BFM_B32 SOP2 +S_BFM_B64 SOP2 +S_BITCMP0_B32 SOPC +S_BITCMP0_B64 SOPC +S_BITCMP1_B32 SOPC +S_BITCMP1_B64 SOPC +S_BITREPLICATE_B64_B32 SOP1 +S_BITSET0_B32 SOP1 +S_BITSET0_B64 SOP1 +S_BITSET1_B32 SOP1 +S_BITSET1_B64 SOP1 +S_BRANCH SOPP +S_BREV_B32 SOP1 +S_BREV_B64 SOP1 +S_BUFFER_LOAD_DWORD SMEM +S_BUFFER_LOAD_DWORDX16 SMEM +S_BUFFER_LOAD_DWORDX2 SMEM +S_BUFFER_LOAD_DWORDX4 SMEM +S_BUFFER_LOAD_DWORDX8 SMEM +S_CALL_B64 SOPK +S_CBRANCH_CDBGSYS SOPP +S_CBRANCH_CDBGSYS_AND_USER SOPP +S_CBRANCH_CDBGSYS_OR_USER SOPP +S_CBRANCH_CDBGUSER SOPP +S_CBRANCH_EXECNZ SOPP +S_CBRANCH_EXECZ SOPP +S_CBRANCH_SCC0 SOPP +S_CBRANCH_SCC1 SOPP +S_CBRANCH_VCCNZ SOPP +S_CBRANCH_VCCZ SOPP +S_CLAUSE SOPP +S_CMOVK_I32 SOPK +S_CMOV_B32 SOP1 +S_CMOV_B64 SOP1 +S_CMPK_EQ_I32 SOPK +S_CMPK_EQ_U32 SOPK +S_CMPK_GE_I32 SOPK +S_CMPK_GE_U32 SOPK +S_CMPK_GT_I32 SOPK +S_CMPK_GT_U32 SOPK +S_CMPK_LE_I32 SOPK +S_CMPK_LE_U32 SOPK +S_CMPK_LG_I32 SOPK +S_CMPK_LG_U32 SOPK +S_CMPK_LT_I32 SOPK +S_CMPK_LT_U32 SOPK +S_CMP_EQ_I32 SOPC +S_CMP_EQ_U32 SOPC +S_CMP_EQ_U64 SOPC +S_CMP_GE_I32 SOPC +S_CMP_GE_U32 SOPC +S_CMP_GT_I32 SOPC +S_CMP_GT_U32 SOPC +S_CMP_LE_I32 SOPC +S_CMP_LE_U32 SOPC +S_CMP_LG_I32 SOPC +S_CMP_LG_U32 SOPC +S_CMP_LG_U64 SOPC +S_CMP_LT_I32 SOPC +S_CMP_LT_U32 SOPC +S_CODE_END SOPP +S_CSELECT_B32 SOP2 +S_CSELECT_B64 SOP2 +S_DCACHE_INV SMEM +S_DECPERFLEVEL SOPP +S_DENORM_MODE SOPP +S_ENDPGM SOPP +S_ENDPGM_ORDERED_PS_DONE SOPP +S_ENDPGM_SAVED SOPP +S_FF0_I32_B32 SOP1 +S_FF0_I32_B64 SOP1 +S_FF1_I32_B32 SOP1 +S_FF1_I32_B64 SOP1 +S_FLBIT_I32 SOP1 +S_FLBIT_I32_B32 SOP1 +S_FLBIT_I32_B64 SOP1 +S_FLBIT_I32_I64 SOP1 +S_GETPC_B64 SOP1 +S_GETREG_B32 SOPK +S_GET_WAVEID_IN_WORKGROUP SMEM +S_GL1_INV SMEM +S_ICACHE_INV SOPP +S_INCPERFLEVEL SOPP +S_INST_PREFETCH SOPP +S_LOAD_DWORD SMEM +S_LOAD_DWORDX16 SMEM +S_LOAD_DWORDX2 SMEM +S_LOAD_DWORDX4 SMEM +S_LOAD_DWORDX8 SMEM +S_LSHL1_ADD_U32 SOP2 +S_LSHL2_ADD_U32 SOP2 +S_LSHL3_ADD_U32 SOP2 +S_LSHL4_ADD_U32 SOP2 +S_LSHL_B32 SOP2 +S_LSHL_B64 SOP2 +S_LSHR_B32 SOP2 +S_LSHR_B64 SOP2 +S_MAX_I32 SOP2 +S_MAX_U32 SOP2 +S_MEMREALTIME SMEM +S_MEMTIME SMEM +S_MIN_I32 SOP2 +S_MIN_U32 SOP2 +S_MOVK_I32 SOPK +S_MOVRELD_B32 SOP1 +S_MOVRELD_B64 SOP1 +S_MOVRELSD_2_B32 SOP1 +S_MOVRELS_B32 SOP1 +S_MOVRELS_B64 SOP1 +S_MOV_B32 SOP1 +S_MOV_B64 SOP1 +S_MULK_I32 SOPK +S_MUL_HI_I32 SOP2 +S_MUL_HI_U32 SOP2 +S_MUL_I32 SOP2 +S_NAND_B32 SOP2 +S_NAND_B64 SOP2 +S_NAND_SAVEEXEC_B32 SOP1 +S_NAND_SAVEEXEC_B64 SOP1 +S_NOP SOPP +S_NOR_B32 SOP2 +S_NOR_B64 SOP2 +S_NOR_SAVEEXEC_B32 SOP1 +S_NOR_SAVEEXEC_B64 SOP1 +S_NOT_B32 SOP1 +S_NOT_B64 SOP1 +S_ORN1_SAVEEXEC_B32 SOP1 +S_ORN1_SAVEEXEC_B64 SOP1 +S_ORN2_B32 SOP2 +S_ORN2_B64 SOP2 +S_ORN2_SAVEEXEC_B32 SOP1 +S_ORN2_SAVEEXEC_B64 SOP1 +S_OR_B32 SOP2 +S_OR_B64 SOP2 +S_OR_SAVEEXEC_B32 SOP1 +S_OR_SAVEEXEC_B64 SOP1 +S_PACK_HH_B32_B16 SOP2 +S_PACK_LH_B32_B16 SOP2 +S_PACK_LL_B32_B16 SOP2 +S_QUADMASK_B32 SOP1 +S_QUADMASK_B64 SOP1 +S_RFE_B64 SOP1 +S_ROUND_MODE SOPP +S_SCRATCH_LOAD_DWORD SMEM +S_SCRATCH_LOAD_DWORDX2 SMEM +S_SCRATCH_LOAD_DWORDX4 SMEM +S_SENDMSG SOPP +S_SENDMSGHALT SOPP +S_SETHALT SOPP +S_SETKILL SOPP +S_SETPC_B64 SOP1 +S_SETPRIO SOPP +S_SETREG_B32 SOPK +S_SETREG_IMM32_B32 SOPK +S_SEXT_I32_I16 SOP1 +S_SEXT_I32_I8 SOP1 +S_SLEEP SOPP +S_SUBB_U32 SOP2 +S_SUBVECTOR_LOOP_BEGIN SOPK +S_SUBVECTOR_LOOP_END SOPK +S_SUB_I32 SOP2 +S_SUB_U32 SOP2 +S_SWAPPC_B64 SOP1 +S_TRAP SOPP +S_TTRACEDATA SOPP +S_TTRACEDATA_IMM SOPP +S_VERSION SOPK +S_WAITCNT SOPP +S_WAITCNT_DEPCTR SOPP +S_WAITCNT_EXPCNT SOPK +S_WAITCNT_LGKMCNT SOPK +S_WAITCNT_VMCNT SOPK +S_WAITCNT_VSCNT SOPK +S_WAIT_IDLE SOPP +S_WAKEUP SOPP +S_WQM_B32 SOP1 +S_WQM_B64 SOP1 +S_XNOR_B32 SOP2 +S_XNOR_B64 SOP2 +S_XNOR_SAVEEXEC_B32 SOP1 +S_XNOR_SAVEEXEC_B64 SOP1 +S_XOR_B32 SOP2 +S_XOR_B64 SOP2 +S_XOR_SAVEEXEC_B32 SOP1 +S_XOR_SAVEEXEC_B64 SOP1 +TBUFFER_LOAD_FORMAT_D16_X MTBUF +TBUFFER_LOAD_FORMAT_D16_XY MTBUF +TBUFFER_LOAD_FORMAT_D16_XYZ MTBUF +TBUFFER_LOAD_FORMAT_D16_XYZW MTBUF +TBUFFER_LOAD_FORMAT_X MTBUF +TBUFFER_LOAD_FORMAT_XY MTBUF +TBUFFER_LOAD_FORMAT_XYZ MTBUF +TBUFFER_LOAD_FORMAT_XYZW MTBUF +TBUFFER_STORE_FORMAT_D16_X MTBUF +TBUFFER_STORE_FORMAT_D16_XY MTBUF +TBUFFER_STORE_FORMAT_D16_XYZ MTBUF +TBUFFER_STORE_FORMAT_D16_XYZW MTBUF +TBUFFER_STORE_FORMAT_X MTBUF +TBUFFER_STORE_FORMAT_XY MTBUF +TBUFFER_STORE_FORMAT_XYZ MTBUF +TBUFFER_STORE_FORMAT_XYZW MTBUF +V_ADD3_U32 VOP3 +V_ADD_CO_CI_U32 VOP2 +V_ADD_CO_U32 VOP3 +V_ADD_F16 VOP2 +V_ADD_F32 VOP2 +V_ADD_F64 VOP3 +V_ADD_LSHL_U32 VOP3 +V_ADD_NC_I16 VOP3 +V_ADD_NC_I32 VOP3 +V_ADD_NC_U16 VOP3 +V_ADD_NC_U32 VOP2 +V_ALIGNBIT_B32 VOP3 +V_ALIGNBYTE_B32 VOP3 +V_AND_B32 VOP2 +V_AND_OR_B32 VOP3 +V_ASHRREV_I16 VOP3 +V_ASHRREV_I32 VOP2 +V_ASHRREV_I64 VOP3 +V_BCNT_U32_B32 VOP3 +V_BFE_I32 VOP3 +V_BFE_U32 VOP3 +V_BFI_B32 VOP3 +V_BFM_B32 VOP3 +V_BFREV_B32 VOP1 +V_CEIL_F16 VOP1 +V_CEIL_F32 VOP1 +V_CEIL_F64 VOP1 +V_CLREXCP VOP1 +V_CMPX_CLASS_F16 VOPC +V_CMPX_CLASS_F32 VOPC +V_CMPX_CLASS_F64 VOPC +V_CMPX_EQ_F16 VOPC +V_CMPX_EQ_F32 VOPC +V_CMPX_EQ_F64 VOPC +V_CMPX_EQ_I16 VOPC +V_CMPX_EQ_I32 VOPC +V_CMPX_EQ_I64 VOPC +V_CMPX_EQ_U16 VOPC +V_CMPX_EQ_U32 VOPC +V_CMPX_EQ_U64 VOPC +V_CMPX_F_F16 VOPC +V_CMPX_F_F32 VOPC +V_CMPX_F_F64 VOPC +V_CMPX_F_I32 VOPC +V_CMPX_F_I64 VOPC +V_CMPX_F_U32 VOPC +V_CMPX_F_U64 VOPC +V_CMPX_GE_F16 VOPC +V_CMPX_GE_F32 VOPC +V_CMPX_GE_F64 VOPC +V_CMPX_GE_I16 VOPC +V_CMPX_GE_I32 VOPC +V_CMPX_GE_I64 VOPC +V_CMPX_GE_U16 VOPC +V_CMPX_GE_U32 VOPC +V_CMPX_GE_U64 VOPC +V_CMPX_GT_F16 VOPC +V_CMPX_GT_F32 VOPC +V_CMPX_GT_F64 VOPC +V_CMPX_GT_I16 VOPC +V_CMPX_GT_I32 VOPC +V_CMPX_GT_I64 VOPC +V_CMPX_GT_U16 VOPC +V_CMPX_GT_U32 VOPC +V_CMPX_GT_U64 VOPC +V_CMPX_LE_F16 VOPC +V_CMPX_LE_F32 VOPC +V_CMPX_LE_F64 VOPC +V_CMPX_LE_I16 VOPC +V_CMPX_LE_I32 VOPC +V_CMPX_LE_I64 VOPC +V_CMPX_LE_U16 VOPC +V_CMPX_LE_U32 VOPC +V_CMPX_LE_U64 VOPC +V_CMPX_LG_F16 VOPC +V_CMPX_LG_F32 VOPC +V_CMPX_LG_F64 VOPC +V_CMPX_LT_F16 VOPC +V_CMPX_LT_F32 VOPC +V_CMPX_LT_F64 VOPC +V_CMPX_LT_I16 VOPC +V_CMPX_LT_I32 VOPC +V_CMPX_LT_I64 VOPC +V_CMPX_LT_U16 VOPC +V_CMPX_LT_U32 VOPC +V_CMPX_LT_U64 VOPC +V_CMPX_NEQ_F16 VOPC +V_CMPX_NEQ_F32 VOPC +V_CMPX_NEQ_F64 VOPC +V_CMPX_NE_I16 VOPC +V_CMPX_NE_I32 VOPC +V_CMPX_NE_I64 VOPC +V_CMPX_NE_U16 VOPC +V_CMPX_NE_U32 VOPC +V_CMPX_NE_U64 VOPC +V_CMPX_NGE_F16 VOPC +V_CMPX_NGE_F32 VOPC +V_CMPX_NGE_F64 VOPC +V_CMPX_NGT_F16 VOPC +V_CMPX_NGT_F32 VOPC +V_CMPX_NGT_F64 VOPC +V_CMPX_NLE_F16 VOPC +V_CMPX_NLE_F32 VOPC +V_CMPX_NLE_F64 VOPC +V_CMPX_NLG_F16 VOPC +V_CMPX_NLG_F32 VOPC +V_CMPX_NLG_F64 VOPC +V_CMPX_NLT_F16 VOPC +V_CMPX_NLT_F32 VOPC +V_CMPX_NLT_F64 VOPC +V_CMPX_O_F16 VOPC +V_CMPX_O_F32 VOPC +V_CMPX_O_F64 VOPC +V_CMPX_TRU_F16 VOPC +V_CMPX_TRU_F32 VOPC +V_CMPX_TRU_F64 VOPC +V_CMPX_T_I32 VOPC +V_CMPX_T_I64 VOPC +V_CMPX_T_U32 VOPC +V_CMPX_T_U64 VOPC +V_CMPX_U_F16 VOPC +V_CMPX_U_F32 VOPC +V_CMPX_U_F64 VOPC +V_CMP_CLASS_F16 VOPC +V_CMP_CLASS_F32 VOPC +V_CMP_CLASS_F64 VOPC +V_CMP_EQ_F16 VOPC +V_CMP_EQ_F32 VOPC +V_CMP_EQ_F64 VOPC +V_CMP_EQ_I16 VOPC +V_CMP_EQ_I32 VOPC +V_CMP_EQ_I64 VOPC +V_CMP_EQ_U16 VOPC +V_CMP_EQ_U32 VOPC +V_CMP_EQ_U64 VOPC +V_CMP_F_F16 VOPC +V_CMP_F_F32 VOPC +V_CMP_F_F64 VOPC +V_CMP_F_I32 VOPC +V_CMP_F_I64 VOPC +V_CMP_F_U32 VOPC +V_CMP_F_U64 VOPC +V_CMP_GE_F16 VOPC +V_CMP_GE_F32 VOPC +V_CMP_GE_F64 VOPC +V_CMP_GE_I16 VOPC +V_CMP_GE_I32 VOPC +V_CMP_GE_I64 VOPC +V_CMP_GE_U16 VOPC +V_CMP_GE_U32 VOPC +V_CMP_GE_U64 VOPC +V_CMP_GT_F16 VOPC +V_CMP_GT_F32 VOPC +V_CMP_GT_F64 VOPC +V_CMP_GT_I16 VOPC +V_CMP_GT_I32 VOPC +V_CMP_GT_I64 VOPC +V_CMP_GT_U16 VOPC +V_CMP_GT_U32 VOPC +V_CMP_GT_U64 VOPC +V_CMP_LE_F16 VOPC +V_CMP_LE_F32 VOPC +V_CMP_LE_F64 VOPC +V_CMP_LE_I16 VOPC +V_CMP_LE_I32 VOPC +V_CMP_LE_I64 VOPC +V_CMP_LE_U16 VOPC +V_CMP_LE_U32 VOPC +V_CMP_LE_U64 VOPC +V_CMP_LG_F16 VOPC +V_CMP_LG_F32 VOPC +V_CMP_LG_F64 VOPC +V_CMP_LT_F16 VOPC +V_CMP_LT_F32 VOPC +V_CMP_LT_F64 VOPC +V_CMP_LT_I16 VOPC +V_CMP_LT_I32 VOPC +V_CMP_LT_I64 VOPC +V_CMP_LT_U16 VOPC +V_CMP_LT_U32 VOPC +V_CMP_LT_U64 VOPC +V_CMP_NEQ_F16 VOPC +V_CMP_NEQ_F32 VOPC +V_CMP_NEQ_F64 VOPC +V_CMP_NE_I16 VOPC +V_CMP_NE_I32 VOPC +V_CMP_NE_I64 VOPC +V_CMP_NE_U16 VOPC +V_CMP_NE_U32 VOPC +V_CMP_NE_U64 VOPC +V_CMP_NGE_F16 VOPC +V_CMP_NGE_F32 VOPC +V_CMP_NGE_F64 VOPC +V_CMP_NGT_F16 VOPC +V_CMP_NGT_F32 VOPC +V_CMP_NGT_F64 VOPC +V_CMP_NLE_F16 VOPC +V_CMP_NLE_F32 VOPC +V_CMP_NLE_F64 VOPC +V_CMP_NLG_F16 VOPC +V_CMP_NLG_F32 VOPC +V_CMP_NLG_F64 VOPC +V_CMP_NLT_F16 VOPC +V_CMP_NLT_F32 VOPC +V_CMP_NLT_F64 VOPC +V_CMP_O_F16 VOPC +V_CMP_O_F32 VOPC +V_CMP_O_F64 VOPC +V_CMP_TRU_F16 VOPC +V_CMP_TRU_F32 VOPC +V_CMP_TRU_F64 VOPC +V_CMP_T_I32 VOPC +V_CMP_T_I64 VOPC +V_CMP_T_U32 VOPC +V_CMP_T_U64 VOPC +V_CMP_U_F16 VOPC +V_CMP_U_F32 VOPC +V_CMP_U_F64 VOPC +V_CNDMASK_B32 VOP2 +V_COS_F16 VOP1 +V_COS_F32 VOP1 +V_CUBEID_F32 VOP3 +V_CUBEMA_F32 VOP3 +V_CUBESC_F32 VOP3 +V_CUBETC_F32 VOP3 +V_CVT_F16_F32 VOP1 +V_CVT_F16_I16 VOP1 +V_CVT_F16_U16 VOP1 +V_CVT_F32_F16 VOP1 +V_CVT_F32_F64 VOP1 +V_CVT_F32_I32 VOP1 +V_CVT_F32_U32 VOP1 +V_CVT_F32_UBYTE0 VOP1 +V_CVT_F32_UBYTE1 VOP1 +V_CVT_F32_UBYTE2 VOP1 +V_CVT_F32_UBYTE3 VOP1 +V_CVT_F64_F32 VOP1 +V_CVT_F64_I32 VOP1 +V_CVT_F64_U32 VOP1 +V_CVT_FLR_I32_F32 VOP1 +V_CVT_I16_F16 VOP1 +V_CVT_I32_F32 VOP1 +V_CVT_I32_F64 VOP1 +V_CVT_NORM_I16_F16 VOP1 +V_CVT_NORM_U16_F16 VOP1 +V_CVT_OFF_F32_I4 VOP1 +V_CVT_PKNORM_I16_F16 VOP3 +V_CVT_PKNORM_I16_F32 VOP3 +V_CVT_PKNORM_U16_F16 VOP3 +V_CVT_PKNORM_U16_F32 VOP3 +V_CVT_PKRTZ_F16_F32 VOP2 +V_CVT_PK_I16_I32 VOP3 +V_CVT_PK_U16_U32 VOP3 +V_CVT_PK_U8_F32 VOP3 +V_CVT_RPI_I32_F32 VOP1 +V_CVT_U16_F16 VOP1 +V_CVT_U32_F32 VOP1 +V_CVT_U32_F64 VOP1 +V_DIV_FIXUP_F16 VOP3 +V_DIV_FIXUP_F32 VOP3 +V_DIV_FIXUP_F64 VOP3 +V_DIV_FMAS_F32 VOP3 +V_DIV_FMAS_F64 VOP3 +V_DIV_SCALE_F32 VOP3 +V_DIV_SCALE_F64 VOP3 +V_DOT2C_F32_F16 VOP2 +V_DOT2_F32_F16 VOP3P +V_DOT2_I32_I16 VOP3P +V_DOT2_U32_U16 VOP3P +V_DOT4C_I32_I8 VOP2 +V_DOT4_I32_I8 VOP3P +V_DOT4_U32_U8 VOP3P +V_DOT8_I32_I4 VOP3P +V_DOT8_U32_U4 VOP3P +V_EXP_F16 VOP1 +V_EXP_F32 VOP1 +V_FFBH_I32 VOP1 +V_FFBH_U32 VOP1 +V_FFBL_B32 VOP1 +V_FLOOR_F16 VOP1 +V_FLOOR_F32 VOP1 +V_FLOOR_F64 VOP1 +V_FMAAK_F16 VOP2 +V_FMAAK_F32 VOP2 +V_FMAC_F16 VOP2 +V_FMAC_F32 VOP2 +V_FMAC_LEGACY_F32 VOP2 +V_FMAMK_F16 VOP2 +V_FMAMK_F32 VOP2 +V_FMA_F16 VOP3 +V_FMA_F32 VOP3 +V_FMA_F64 VOP3 +V_FMA_LEGACY_F32 VOP3 +V_FMA_MIXHI_F16 VOP3P +V_FMA_MIXLO_F16 VOP3P +V_FMA_MIX_F32 VOP3P +V_FRACT_F16 VOP1 +V_FRACT_F32 VOP1 +V_FRACT_F64 VOP1 +V_FREXP_EXP_I16_F16 VOP1 +V_FREXP_EXP_I32_F32 VOP1 +V_FREXP_EXP_I32_F64 VOP1 +V_FREXP_MANT_F16 VOP1 +V_FREXP_MANT_F32 VOP1 +V_FREXP_MANT_F64 VOP1 +V_INTERP_MOV_F32 VINTRP +V_INTERP_P1LL_F16 VOP3 +V_INTERP_P1LV_F16 VOP3 +V_INTERP_P1_F32 VINTRP +V_INTERP_P2_F16 VOP3 +V_INTERP_P2_F32 VINTRP +V_LDEXP_F16 VOP2 +V_LDEXP_F32 VOP3 +V_LDEXP_F64 VOP3 +V_LERP_U8 VOP3 +V_LOG_F16 VOP1 +V_LOG_F32 VOP1 +V_LSHLREV_B16 VOP3 +V_LSHLREV_B32 VOP2 +V_LSHLREV_B64 VOP3 +V_LSHL_ADD_U32 VOP3 +V_LSHL_OR_B32 VOP3 +V_LSHRREV_B16 VOP3 +V_LSHRREV_B32 VOP2 +V_LSHRREV_B64 VOP3 +V_MAC_F32 VOP2 +V_MAC_LEGACY_F32 VOP2 +V_MADAK_F32 VOP2 +V_MADMK_F32 VOP2 +V_MAD_F32 VOP3 +V_MAD_I16 VOP3 +V_MAD_I32_I16 VOP3 +V_MAD_I32_I24 VOP3 +V_MAD_I64_I32 VOP3 +V_MAD_LEGACY_F32 VOP3 +V_MAD_U16 VOP3 +V_MAD_U32_U16 VOP3 +V_MAD_U32_U24 VOP3 +V_MAD_U64_U32 VOP3 +V_MAX3_F16 VOP3 +V_MAX3_F32 VOP3 +V_MAX3_I16 VOP3 +V_MAX3_I32 VOP3 +V_MAX3_U16 VOP3 +V_MAX3_U32 VOP3 +V_MAX_F16 VOP2 +V_MAX_F32 VOP2 +V_MAX_F64 VOP3 +V_MAX_I16 VOP3 +V_MAX_I32 VOP2 +V_MAX_U16 VOP3 +V_MAX_U32 VOP2 +V_MBCNT_HI_U32_B32 VOP3 +V_MBCNT_LO_U32_B32 VOP3 +V_MED3_F16 VOP3 +V_MED3_F32 VOP3 +V_MED3_I16 VOP3 +V_MED3_I32 VOP3 +V_MED3_U16 VOP3 +V_MED3_U32 VOP3 +V_MIN3_F16 VOP3 +V_MIN3_F32 VOP3 +V_MIN3_I16 VOP3 +V_MIN3_I32 VOP3 +V_MIN3_U16 VOP3 +V_MIN3_U32 VOP3 +V_MIN_F16 VOP2 +V_MIN_F32 VOP2 +V_MIN_F64 VOP3 +V_MIN_I16 VOP3 +V_MIN_I32 VOP2 +V_MIN_U16 VOP3 +V_MIN_U32 VOP2 +V_MOVRELD_B32 VOP1 +V_MOVRELSD_2_B32 VOP1 +V_MOVRELSD_B32 VOP1 +V_MOVRELS_B32 VOP1 +V_MOV_B32 VOP1 +V_MQSAD_PK_U16_U8 VOP3 +V_MQSAD_U32_U8 VOP3 +V_MSAD_U8 VOP3 +V_MULLIT_F32 VOP3 +V_MUL_F16 VOP2 +V_MUL_F32 VOP2 +V_MUL_F64 VOP3 +V_MUL_HI_I32 VOP3 +V_MUL_HI_I32_I24 VOP2 +V_MUL_HI_U32 VOP3 +V_MUL_HI_U32_U24 VOP2 +V_MUL_I32_I24 VOP2 +V_MUL_LEGACY_F32 VOP2 +V_MUL_LO_U16 VOP3 +V_MUL_LO_U32 VOP3 +V_MUL_U32_U24 VOP2 +V_NOP VOP1 +V_NOT_B32 VOP1 +V_OR3_B32 VOP3 +V_OR_B32 VOP2 +V_PACK_B32_F16 VOP3 +V_PERMLANE16_B32 VOP3 +V_PERMLANEX16_B32 VOP3 +V_PERM_B32 VOP3 +V_PIPEFLUSH VOP1 +V_PK_ADD_F16 VOP3P +V_PK_ADD_I16 VOP3P +V_PK_ADD_U16 VOP3P +V_PK_ASHRREV_I16 VOP3P +V_PK_FMAC_F16 VOP2 +V_PK_FMA_F16 VOP3P +V_PK_LSHLREV_B16 VOP3P +V_PK_LSHRREV_B16 VOP3P +V_PK_MAD_I16 VOP3P +V_PK_MAD_U16 VOP3P +V_PK_MAX_F16 VOP3P +V_PK_MAX_I16 VOP3P +V_PK_MAX_U16 VOP3P +V_PK_MIN_F16 VOP3P +V_PK_MIN_I16 VOP3P +V_PK_MIN_U16 VOP3P +V_PK_MUL_F16 VOP3P +V_PK_MUL_LO_U16 VOP3P +V_PK_SUB_I16 VOP3P +V_PK_SUB_U16 VOP3P +V_QSAD_PK_U16_U8 VOP3 +V_RCP_F16 VOP1 +V_RCP_F32 VOP1 +V_RCP_F64 VOP1 +V_RCP_IFLAG_F32 VOP1 +V_READFIRSTLANE_B32 VOP1 +V_READLANE_B32 VOP3 +V_RNDNE_F16 VOP1 +V_RNDNE_F32 VOP1 +V_RNDNE_F64 VOP1 +V_RSQ_F16 VOP1 +V_RSQ_F32 VOP1 +V_RSQ_F64 VOP1 +V_SAD_HI_U8 VOP3 +V_SAD_U16 VOP3 +V_SAD_U32 VOP3 +V_SAD_U8 VOP3 +V_SAT_PK_U8_I16 VOP1 +V_SIN_F16 VOP1 +V_SIN_F32 VOP1 +V_SQRT_F16 VOP1 +V_SQRT_F32 VOP1 +V_SQRT_F64 VOP1 +V_SUBREV_CO_CI_U32 VOP2 +V_SUBREV_CO_U32 VOP3 +V_SUBREV_F16 VOP2 +V_SUBREV_F32 VOP2 +V_SUBREV_NC_U32 VOP2 +V_SUB_CO_CI_U32 VOP2 +V_SUB_CO_U32 VOP3 +V_SUB_F16 VOP2 +V_SUB_F32 VOP2 +V_SUB_NC_I16 VOP3 +V_SUB_NC_I32 VOP3 +V_SUB_NC_U16 VOP3 +V_SUB_NC_U32 VOP2 +V_SWAPREL_B32 VOP1 +V_SWAP_B32 VOP1 +V_TRIG_PREOP_F64 VOP3 +V_TRUNC_F16 VOP1 +V_TRUNC_F32 VOP1 +V_TRUNC_F64 VOP1 +V_WRITELANE_B32 VOP3 +V_XAD_U32 VOP3 +V_XNOR_B32 VOP2 +V_XOR3_B32 VOP3 +V_XOR_B32 VOP2 From e73cd7dec366fd2616a570979e9dd4a2e5a65041 Mon Sep 17 00:00:00 2001 From: adriadam10 Date: Sun, 27 Sep 2026 18:47:32 +0200 Subject: [PATCH 04/46] feat(tools): report progress changes on pull requests progress.py stores the names of implemented and pending items, can measure another source tree with --root and prints a Markdown diff of two runs with --compare. The Progress report workflow compares every pull request with its base, and Progress comment posts the result as a single comment, updated on every push and removed when progress no longer changes. Commenting runs as a separate workflow_run so pull requests from forks never run with write permissions. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/progress-comment.yml | 42 ++++++++++++++++ .github/workflows/progress-report.yml | 34 +++++++++++++ tools/README.md | 12 ++++- tools/progress.py | 67 ++++++++++++++++++++++++-- 4 files changed, 149 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/progress-comment.yml create mode 100644 .github/workflows/progress-report.yml diff --git a/.github/workflows/progress-comment.yml b/.github/workflows/progress-comment.yml new file mode 100644 index 000000000..05568e459 --- /dev/null +++ b/.github/workflows/progress-comment.yml @@ -0,0 +1,42 @@ +name: Progress comment + +on: + workflow_run: + workflows: [Progress report] + types: [completed] + +permissions: + actions: read + pull-requests: write + +jobs: + comment: + if: github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v8 + with: + name: progress-report + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + - env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RUN_SHA: ${{ github.event.workflow_run.head_sha }} + run: | + pr=$(cat pr) + [[ $pr =~ ^[0-9]+$ ]] || exit 1 + [ "$(gh pr view "$pr" --json headRefOid --jq .headRefOid)" = "$RUN_SHA" ] || exit 0 + marker='' + id=$(gh api "repos/$GH_REPO/issues/$pr/comments" --paginate \ + --jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | startswith(\"$marker\"))) | .id" | head -n1) + if [ -f report.md ]; then + printf '%s\n%s' "$marker" "$(cat report.md)" > body.md + if [ -n "$id" ]; then + gh api -X PATCH "repos/$GH_REPO/issues/comments/$id" -F body=@body.md > /dev/null + else + gh pr comment "$pr" --body-file body.md + fi + elif [ -n "$id" ]; then + gh api -X DELETE "repos/$GH_REPO/issues/comments/$id" + fi diff --git a/.github/workflows/progress-report.yml b/.github/workflows/progress-report.yml new file mode 100644 index 000000000..3fbf7361b --- /dev/null +++ b/.github/workflows/progress-report.yml @@ -0,0 +1,34 @@ +name: Progress report + +on: + pull_request: + +permissions: + contents: read + +jobs: + report: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 2 + - env: + PR: ${{ github.event.pull_request.number }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + out=$RUNNER_TEMP/out report=$RUNNER_TEMP/report + git worktree add --detach "$RUNNER_TEMP/base" HEAD^1 + python3 tools/progress.py --root "$RUNNER_TEMP/base" "$out/base" + python3 tools/progress.py "$out/head" + mkdir "$report" + echo "$PR" > "$report/pr" + body=$(python3 tools/progress.py --compare "$out/base/progress.json" "$out/head/progress.json") + if [ -n "$body" ]; then + printf '### Progress report (%s → %s)\n\n%s\n' "$(git rev-parse --short HEAD^1)" "${HEAD_SHA::7}" "$body" > "$report/report.md" + cat "$report/report.md" >> "$GITHUB_STEP_SUMMARY" + fi + - uses: actions/upload-artifact@v7 + with: + name: progress-report + path: ${{ runner.temp }}/report diff --git a/tools/README.md b/tools/README.md index 5a1481df6..b374dc398 100644 --- a/tools/README.md +++ b/tools/README.md @@ -13,10 +13,20 @@ Requires only Python 3.9+ with the standard library. It writes: - `progress.svg`: treemaps for system libraries and GPU shader instructions - `badge-libraries.svg`, `badge-shaders.svg`: percentage badges - `index.html`: the map with per-library and per-encoding tables -- `progress.json`: raw numbers +- `progress.json`: raw numbers and the names of implemented and pending items The [Progress workflow](../.github/workflows/progress.yml) runs it on every push to `main` and deploys the output to GitHub Pages, so nothing generated is committed to the repository. +To compare two runs, for example `main` and a pull request: + +```sh +python3 tools/progress.py --root base +python3 tools/progress.py head +python3 tools/progress.py --compare base/progress.json head/progress.json +``` + +`--root` measures another source tree with the current script. `--compare` prints a Markdown report of implemented, newly declared, reverted and removed items, and prints nothing when progress did not change. The [Progress report workflow](../.github/workflows/progress-report.yml) runs it for every pull request, and [Progress comment](../.github/workflows/progress-comment.yml) posts the report as a single comment on the pull request, updated on every push. It runs as a separate `workflow_run` so pull requests from forks never run with write permissions. + ### What is measured - **System libraries**: functions defined in [core/libs/prx](../core/libs/prx). A function is implemented when it no longer calls `NotImplemented_nid_no_patch`. The total only includes functions already declared in the project, not every function of the PS5 firmware. diff --git a/tools/progress.py b/tools/progress.py index d90d7b6af..2af9611d1 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -24,6 +24,7 @@ OPCODE_ALIASES = { "VMadMixloF16": "V_FMA_MIXLO_F16", "VMadMixhiF16": "V_FMA_MIXHI_F16", } +REPORT_ROWS = 100 PANEL_WIDTH, GAP, MAP_HEIGHT, HEADER = 495, 10, 280, 30 DONE_COLOR, TODO_COLOR, BORDER, TEXT = "#2ea043", "#1f6feb", "#0d1117", "#ffffff" @@ -51,7 +52,8 @@ def scan_library(path): body = text[match.end() - 1:body_end(text, match.end() - 1)] (todo if STUB in body else done).add(name) todo -= done - return {"name": path.name, "label": path.name.removeprefix("libSce"), "done": len(done), "todo": len(todo)} + return {"name": path.name, "label": path.name.removeprefix("libSce"), "done": len(done), "todo": len(todo), + "done_names": sorted(done), "todo_names": sorted(todo)} def summarize(groups): @@ -88,8 +90,11 @@ def collect_shaders(): extra.append(opcode) groups = {} for name, encoding in isa.items(): - group = groups.setdefault(encoding, {"name": encoding, "label": encoding, "done": 0, "todo": 0}) - group["done" if name in supported else "todo"] += 1 + group = groups.setdefault(encoding, {"name": encoding, "label": encoding, "done": 0, "todo": 0, + "done_names": [], "todo_names": []}) + state = "done" if name in supported else "todo" + group[state] += 1 + group[f"{state}_names"].append(name) result = summarize(sorted(groups.values(), key=lambda g: g["name"])) result["extra"] = extra return result @@ -226,10 +231,62 @@ def summary(libraries, shaders): ]) + "\n" +def names(data, state): + return {(g["name"], n) for g in data["groups"] for n in g.get(f"{state}_names", [])} + + +def details(icon, title, column, items): + if not items: + return [] + rows = [f"
\n{icon} {len(items)} {title}\n", f"| {column} | Name |", "| - | - |"] + rows += [f"| {group} | `{name}` |" for group, name in sorted(items)[:REPORT_ROWS]] + if len(items) > REPORT_ROWS: + rows.append(f"| ... | {len(items) - REPORT_ROWS} more |") + return rows + ["\n
"] + + +def compare(title, column, unit, base, head): + base_done, head_done = names(base, "done"), names(head, "done") + base_all, head_all = base_done | names(base, "todo"), head_done | names(head, "todo") + implemented, declared = head_done - base_done, head_all - base_all - head_done + regressed, removed = base_done & (head_all - head_done), base_all - head_all + if not (implemented or declared or regressed or removed): + return [] + delta = round(head["percent"] - base["percent"], 2) + icon = "📈" if delta > 0 else "📉" if delta < 0 else "➖" + counts = [f"{n:+} {label}" for n, label in ((len(implemented), "implemented"), (len(declared), "declared"), + (-len(removed), "removed")) if n] + lines = [f'{icon} **{title}**: {head["percent"]}% ({delta:+}%, {", ".join(counts)} {unit})', ""] + lines += details("✅", "implemented", column, implemented) + lines += details("🆕", "declared as stubs", column, declared) + lines += details("⚠️", "went back to stubs", column, regressed) + lines += details("🗑️", "removed", column, removed) + return lines + [""] + + +def report(base, head): + lines = compare("System libraries", "Library", "functions", base["libraries"], head["libraries"]) + lines += compare("GPU shader instructions", "Encoding", "instructions", base["shaders"], head["shaders"]) + return "\n".join(lines) + + if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument("output", type=Path) - output = parser.parse_args().output + parser.add_argument("output", type=Path, nargs="?") + parser.add_argument("--root", type=Path, help="source tree to measure instead of the one containing this script") + parser.add_argument("--compare", type=Path, nargs=2, metavar=("BASE", "HEAD"), + help="print a markdown report of the changes between two progress.json files") + args = parser.parse_args() + if args.compare: + base, head = (json.loads(path.read_text()) for path in args.compare) + print(report(base, head), end="") + raise SystemExit + if not args.output: + parser.error("the output directory is required") + if args.root: + PRX = args.root / "core" / "libs" / "prx" + OPCODES = args.root / OPCODES.relative_to(ROOT) + output = args.output output.mkdir(parents=True, exist_ok=True) libraries, shaders = collect_libraries(), collect_shaders() (output / "progress.json").write_text(json.dumps({"libraries": libraries, "shaders": shaders}, indent=2)) From 00e4f7678da1bfe783dbe595145093a4a5028336 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 28 Sep 2026 16:44:52 +1300 Subject: [PATCH 05/46] feat(font): port shadPS4's sceFont and sceFontFt on FreeType Ported from shadPS4 (GPL-2.0-or-later) with system fonts removed, stubs made to throw, and sceFontCloseFont implemented. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01ButHENCbXXt8h4EZuWUabJ --- core/libs/CMakeLists.txt | 8 + core/libs/SceTypes.hpp | 201 ----- .../libs/prx/libSceFont.native/CMakeLists.txt | 2 + core/libs/prx/libSceFont.native/Export.cpp | 520 ------------- core/libs/prx/libSceFont/CMakeLists.txt | 2 + core/libs/prx/libSceFont/Export.cpp | 520 ------------- core/libs/prx/libSceFont/FontLibrary.cmake | 23 + .../prx/libSceFont/include/FontDriver.hpp | 360 +++++++++ .../prx/libSceFont/include/FontFreeType.hpp | 151 ++++ .../prx/libSceFont/include/FontInternal.hpp | 112 +++ .../libs/prx/libSceFont/include/FontTypes.hpp | 432 +++++++++++ core/libs/prx/libSceFont/src/Layout.cpp | 522 +++++++++++++ core/libs/prx/libSceFont/src/Library.cpp | 378 ++++++++++ core/libs/prx/libSceFont/src/Open.cpp | 509 +++++++++++++ core/libs/prx/libSceFont/src/Render.cpp | 449 +++++++++++ core/libs/prx/libSceFont/src/State.cpp | 433 +++++++++++ core/libs/prx/libSceFont/src/Style.cpp | 303 ++++++++ core/libs/prx/libSceFont/src/Text.cpp | 633 ++++++++++++++++ .../libs/prx/libSceFont/src/Unimplemented.cpp | 477 ++++++++++++ core/libs/prx/libSceFontFt/CMakeLists.txt | 15 + core/libs/prx/libSceFontFt/Export.cpp | 118 ++- .../prx/libSceFontFt/include/FontFtDriver.hpp | 16 + core/libs/prx/libSceFontFt/src/Driver.cpp | 711 ++++++++++++++++++ core/libs/tests/GuestFont.cpp | 75 ++ 24 files changed, 5718 insertions(+), 1252 deletions(-) create mode 100644 core/libs/prx/libSceFont.native/CMakeLists.txt delete mode 100644 core/libs/prx/libSceFont.native/Export.cpp create mode 100644 core/libs/prx/libSceFont/CMakeLists.txt delete mode 100644 core/libs/prx/libSceFont/Export.cpp create mode 100644 core/libs/prx/libSceFont/FontLibrary.cmake create mode 100644 core/libs/prx/libSceFont/include/FontDriver.hpp create mode 100644 core/libs/prx/libSceFont/include/FontFreeType.hpp create mode 100644 core/libs/prx/libSceFont/include/FontInternal.hpp create mode 100644 core/libs/prx/libSceFont/include/FontTypes.hpp create mode 100644 core/libs/prx/libSceFont/src/Layout.cpp create mode 100644 core/libs/prx/libSceFont/src/Library.cpp create mode 100644 core/libs/prx/libSceFont/src/Open.cpp create mode 100644 core/libs/prx/libSceFont/src/Render.cpp create mode 100644 core/libs/prx/libSceFont/src/State.cpp create mode 100644 core/libs/prx/libSceFont/src/Style.cpp create mode 100644 core/libs/prx/libSceFont/src/Text.cpp create mode 100644 core/libs/prx/libSceFont/src/Unimplemented.cpp create mode 100644 core/libs/prx/libSceFontFt/CMakeLists.txt create mode 100644 core/libs/prx/libSceFontFt/include/FontFtDriver.hpp create mode 100644 core/libs/prx/libSceFontFt/src/Driver.cpp create mode 100644 core/libs/tests/GuestFont.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index b0572b5bf..c5bc6746d 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -310,6 +310,14 @@ if(BUILD_TESTING) set_tests_properties(guest_rtc PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_executable(guest_font_tests tests/GuestFont.cpp) + target_include_directories(guest_font_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_font_tests PRIVATE libSceFont libSceFontFt libc) + add_test(NAME guest_font COMMAND guest_font_tests) + if(WIN32) + set_tests_properties(guest_font PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(mspace_tests tests/Mspace.cpp) target_include_directories(mspace_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(mspace_tests PRIVATE libc) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 51b0a9caf..2328a171c 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1150,207 +1150,6 @@ struct PositionAndForm { std::uint32_t height; }; -using FontHandle = void*; -using FontLibrary = void*; -using FontLibrarySelection = void*; -using FontRenderer = void*; -using FontRendererSelection = void*; - -using FontMemoryDestroyCallback = void (*)(void*); - -struct FontMemoryInterface { - void* (*malloc_func)(std::size_t); - void (*free_func)(void*); -}; - -struct FontMemory { - std::uint16_t type; - std::uint16_t attr; - std::uint32_t size; - void* address; - void* mspace_object; - const FontMemoryInterface* mem_interface; - FontMemoryDestroyCallback destroy_callback; - void* destroy_object; - void* user_object; - void* parent_object; -}; - -using FontOpenDetail = void; - -struct FontHorizontalLayout { - float base_line_y; - float line_height; - float effect_height; -}; - -struct FontVerticalLayout { - float base_line_x; - float line_width; - float effect_width; -}; - -struct FontGlyphMetrics { - float width; - float height; - struct { - float bearing_x; - float bearing_y; - float advance; - } horizontal; - struct { - float bearing_x; - float bearing_y; - float advance; - } vertical; -}; - -struct FontRenderSurface { - void* buffer; - std::int32_t width_byte; - std::int8_t pixel_size_byte; - std::uint8_t system_ext0; - std::uint8_t system_ext1; - std::uint8_t system_ext2; - std::int32_t width; - std::int32_t height; - struct { - std::uint32_t x0; - std::uint32_t y0; - std::uint32_t x1; - std::uint32_t y1; - } scissor; - std::uint32_t system_use[22]; -}; - -struct FontRenderCharacter { std::uint8_t reserved[128]; }; - -struct FontTransImage { - std::uint8_t* address; - std::uint32_t width_byte; - std::uint32_t image_width; - std::uint32_t image_height; -}; - -struct FontSurfaceImage { - std::uint8_t* address; - std::uint32_t width_byte; - std::uint8_t pixel_size_byte; - std::uint8_t pixel_format; -}; - -struct FontGlyphImageMetrics { - float bearing_x; - float bearing_y; - float advance; - float stride; - std::uint32_t width; - std::uint32_t height; -}; - -struct FontRenderResult { - const FontTransImage* trans_image; - FontSurfaceImage surface_image; - struct { - std::uint32_t x; - std::uint32_t y; - std::uint32_t w; - std::uint32_t h; - } update_rect; - FontGlyphImageMetrics image_metrics; -}; - -using FontCreateStringOrdersFunction = void (*)(void*, void*); - -struct FontCreateStringDetail { - std::uint16_t detail_id; - std::uint8_t detail_type; - std::uint8_t detections; - std::uint32_t orders_option; - FontHandle default_font; - struct { - FontCreateStringOrdersFunction function; - void* object; - } orders; -}; - -struct FontGenerateGlyphDetail { std::uint8_t reserved[64]; }; - -struct FontString { void* system_use[32]; }; - -struct FontWriting { void* system_use[32]; }; - -struct FontWritingExtent { - float top; - float bottom; - float left; - float right; -}; - -struct FontWritingMetrics { - float advance_x; - float advance_y; - FontWritingExtent extent; -}; - -struct FontWritingStep { - float x; - float y; - float advance_x; - float advance_y; - FontHandle font; - struct { - std::uint32_t character_count : 8; - std::uint32_t invisible_glyph : 1; - } profile; - std::uint32_t glyph_code; - struct { - float x; - float y; - } positioning; - FontGlyphMetrics glyph_metrics; -}; - -struct FontWritingLine { std::uint8_t reserved[128]; }; - -struct FontWritingLineStep { - float x; - float y; - float advance_x; - float advance_y; - float spacing_progress; - void* writing_orderer; - struct { - float x; - float y; - } adjusting; - FontWritingMetrics metrics; -}; - -using FontTextParseFunction = int (*)(void*, std::uint32_t, void*); - -struct FontTextSource { - std::uint64_t system_use0; - const void* start; - const void* end; - const void* current; - FontTextParseFunction text_parser; - void* text_object; - FontHandle default_font; - void* system_use[5]; -}; - -struct FontTextCharacter { std::uint8_t reserved[64]; }; - -union FontTextCodes { - struct { - void* order; - std::uint32_t code; - std::uint32_t reserved; - } text; - void* system_reserved[8]; -}; - struct NetEtherAddr { std::uint8_t data[6]; }; union NetEpollData { diff --git a/core/libs/prx/libSceFont.native/CMakeLists.txt b/core/libs/prx/libSceFont.native/CMakeLists.txt new file mode 100644 index 000000000..af4d2df01 --- /dev/null +++ b/core/libs/prx/libSceFont.native/CMakeLists.txt @@ -0,0 +1,2 @@ +include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceFont/FontLibrary.cmake) +add_sce_font_library(libSceFont.native) diff --git a/core/libs/prx/libSceFont.native/Export.cpp b/core/libs/prx/libSceFont.native/Export.cpp deleted file mode 100644 index 33f3d0fa4..000000000 --- a/core/libs/prx/libSceFont.native/Export.cpp +++ /dev/null @@ -1,520 +0,0 @@ -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - -extern "C" { - -int APS5_VABI sceFontAttachDeviceCacheBuffer(FontLibrary library, void* buffer, uint32_t size) { - (void)library; - (void)buffer; - (void)size; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontBindRenderer(FontHandle font_handle, FontRenderer renderer) { - (void)font_handle; - (void)renderer; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontCharacterGetBidiLevel(FontTextCharacter text_character, int* bidi_level) { - (void)text_character; - (void)bidi_level; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontCharacterGetSyllableStringState(FontTextCharacter text_character, int* syllable_string_state) { - (void)text_character; - (void)syllable_string_state; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontCharacterGetTextFontCode(FontTextCharacter text_character, FontHandle* font_handle, uint32_t* text_code) { - (void)text_character; - (void)font_handle; - (void)text_code; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontCharacterGetTextOrder(FontTextCharacter text_character, void** text_order) { - (void)text_character; - (void)text_order; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -uint32_t APS5_VABI sceFontCharacterLooksFormatCharacters(FontTextCharacter text_character) { - (void)text_character; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -uint32_t APS5_VABI sceFontCharacterLooksWhiteSpace(FontTextCharacter text_character) { - (void)text_character; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -FontTextCharacter APS5_VABI sceFontCharacterRefersTextBack(FontTextCharacter text_character) { - (void)text_character; - NotImplemented_nid_no_patch(__func__); - return {}; -} - -FontTextCharacter APS5_VABI sceFontCharacterRefersTextNext(FontTextCharacter text_character) { - (void)text_character; - NotImplemented_nid_no_patch(__func__); - return {}; -} - -FontTextCodes* APS5_VABI sceFontCharactersRefersTextCodes(FontTextCharacter text_character, FontTextCharacter term_character, FontTextCodes* text_codes) { - (void)text_character; - (void)term_character; - (void)text_codes; - NotImplemented_nid_no_patch(__func__); - return nullptr; -} - -int APS5_VABI sceFontCloseFont(FontHandle font_handle) { - (void)font_handle; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontCreateLibrary(const FontMemory* memory, FontLibrarySelection selection, FontLibrary* library) { - (void)memory; - (void)selection; - (void)library; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontCreateLibraryWithEdition(const FontMemory* memory, FontLibrarySelection selection, uint64_t edition, FontLibrary* library) { - (void)memory; - (void)selection; - (void)edition; - (void)library; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontCreateRendererWithEdition(const FontMemory* memory, FontRendererSelection selection, uint64_t edition, FontRenderer* renderer) { - (void)memory; - (void)selection; - (void)edition; - (void)renderer; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontCreateString(const FontMemory* memory, FontTextSource* font_text_source, const FontCreateStringDetail* string_detail, FontString* font_string) { - (void)memory; - (void)font_text_source; - (void)string_detail; - (void)font_string; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontCreateWritingLine(const FontMemory* memory, int writing_form, const void* writing_line_detail, FontWritingLine* writing_line) { - (void)memory; - (void)writing_form; - (void)writing_line_detail; - (void)writing_line; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontDefineAttribute(FontHandle font_handle, int attribute, int* old_attribute) { - (void)font_handle; - (void)attribute; - (void)old_attribute; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontDeleteGlyph(const FontMemory* memory, void** font_glyph) { - (void)memory; - (void)font_glyph; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontDestroyLibrary(FontLibrary* library) { - (void)library; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontDestroyRenderer(FontRenderer* renderer) { - (void)renderer; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontDestroyString(FontString* font_string) { - (void)font_string; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontDestroyWritingLine(FontWritingLine* writing_line) { - (void)writing_line; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGenerateCharGlyph(FontHandle font_handle, uint32_t code, const FontGenerateGlyphDetail* detail, void** font_glyph) { - (void)font_handle; - (void)code; - (void)detail; - (void)font_glyph; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGetCharGlyphMetrics(FontHandle font_handle, uint32_t code, FontGlyphMetrics* metrics) { - (void)font_handle; - (void)code; - (void)metrics; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGetHorizontalLayout(FontHandle font_handle, FontHorizontalLayout* layout) { - (void)font_handle; - (void)layout; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGetRenderCharGlyphMetrics(FontHandle font_handle, uint32_t code, FontGlyphMetrics* metrics) { - (void)font_handle; - (void)code; - (void)metrics; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGetVerticalLayout(FontHandle font_handle, FontVerticalLayout* layout) { - (void)font_handle; - (void)layout; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGlyphDefineAttribute(void* font_glyph, int attribute, int* old_attribute) { - (void)font_glyph; - (void)attribute; - (void)old_attribute; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontMemoryInit(FontMemory* font_memory, void* address, uint32_t size_byte, const FontMemoryInterface* mem_interface, void* mspace_object, FontMemoryDestroyCallback destroy_callback, void* destroy_object) { - (void)font_memory; - (void)address; - (void)size_byte; - (void)mem_interface; - (void)mspace_object; - (void)destroy_callback; - (void)destroy_object; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontMemoryTerm(FontMemory* font_memory) { - (void)font_memory; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontOpenFontInstance(FontHandle font_handle, void* setup_font, FontHandle* out_font_handle) { - (void)font_handle; - (void)setup_font; - (void)out_font_handle; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontOpenFontMemory(FontLibrary library, const void* font_address, uint32_t font_size, const FontOpenDetail* detail, FontHandle* handle) { - (void)library; - (void)font_address; - (void)font_size; - (void)detail; - (void)handle; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontOpenFontSet(FontLibrary library, uint32_t font_set_type, uint32_t open_mode, const FontOpenDetail* detail, FontHandle* handle) { - (void)library; - (void)font_set_type; - (void)open_mode; - (void)detail; - (void)handle; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontRebindRenderer(FontHandle font_handle) { - (void)font_handle; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontRenderCharGlyphImageHorizontal(FontHandle font_handle, uint32_t code, FontRenderSurface* surf, float x, float y, FontGlyphMetrics* metrics, FontRenderResult* result) { - (void)font_handle; - (void)code; - (void)surf; - (void)x; - (void)y; - (void)metrics; - (void)result; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -void APS5_VABI sceFontRenderSurfaceInit(FontRenderSurface* surf, void* buffer, int buf_width_byte, int pixel_size_byte, int width, int height) { - (void)surf; - (void)buffer; - (void)buf_width_byte; - (void)pixel_size_byte; - (void)width; - (void)height; - NotImplemented_nid_no_patch(__func__); -} - -void APS5_VABI sceFontRenderSurfaceSetScissor(FontRenderSurface* surf, uint32_t x0, uint32_t y0, uint32_t x1, uint32_t y1) { - (void)surf; - (void)x0; - (void)y0; - (void)x1; - (void)y1; - NotImplemented_nid_no_patch(__func__); -} - -int APS5_VABI sceFontSetEffectSlant(FontHandle font_handle, float slant_ratio) { - (void)font_handle; - (void)slant_ratio; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontSetEffectWeight(FontHandle font_handle, float weight_x_scale, float weight_y_scale, uint32_t mode) { - (void)font_handle; - (void)weight_x_scale; - (void)weight_y_scale; - (void)mode; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontSetScalePixel(FontHandle font_handle, float w, float h) { - (void)font_handle; - (void)w; - (void)h; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontSetupRenderEffectSlant(FontHandle font_handle, float slant_ratio) { - (void)font_handle; - (void)slant_ratio; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontSetupRenderEffectWeight(FontHandle font_handle, float weight_x_scale, float weight_y_scale, uint32_t mode) { - (void)font_handle; - (void)weight_x_scale; - (void)weight_y_scale; - (void)mode; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontSetupRenderScalePixel(FontHandle font_handle, float w, float h) { - (void)font_handle; - (void)w; - (void)h; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -uint32_t APS5_VABI sceFontStringGetTerminateCode(FontString font_string) { - (void)font_string; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -void* APS5_VABI sceFontStringGetTerminateOrder(FontString font_string) { - (void)font_string; - NotImplemented_nid_no_patch(__func__); - return nullptr; -} - -int APS5_VABI sceFontStringGetWritingForm(FontString font_string) { - (void)font_string; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -FontRenderCharacter APS5_VABI sceFontStringRefersRenderCharacters(FontString font_string, FontTextCharacter start_character, FontTextCharacter last_character, uint32_t* character_count) { - (void)font_string; - (void)start_character; - (void)last_character; - (void)character_count; - NotImplemented_nid_no_patch(__func__); - return {}; -} - -FontTextCharacter APS5_VABI sceFontStringRefersTextCharacters(FontString font_string, uint32_t* character_count) { - (void)font_string; - (void)character_count; - NotImplemented_nid_no_patch(__func__); - return {}; -} - -int APS5_VABI sceFontSupportExternalFonts(FontLibrary library, uint32_t font_max, uint32_t formats) { - (void)library; - (void)font_max; - (void)formats; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontSupportSystemFonts(FontLibrary library) { - (void)library; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -FontTextCodes* APS5_VABI sceFontTextCodesStepBack(FontTextCodes* text_codes_step) { - (void)text_codes_step; - NotImplemented_nid_no_patch(__func__); - return nullptr; -} - -FontTextCodes* APS5_VABI sceFontTextCodesStepNext(FontTextCodes* text_codes_step) { - (void)text_codes_step; - NotImplemented_nid_no_patch(__func__); - return nullptr; -} - -int APS5_VABI sceFontTextSourceInit(FontTextSource* font_text_source, const void* text_address, uint32_t text_size_byte, FontTextParseFunction text_parser, void* text_object) { - (void)font_text_source; - (void)text_address; - (void)text_size_byte; - (void)text_parser; - (void)text_object; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontTextSourceRewind(FontTextSource* font_text_source) { - (void)font_text_source; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontTextSourceSetDefaultFont(FontTextSource* font_text_source, FontHandle default_font) { - (void)font_text_source; - (void)default_font; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontTextSourceSetWritingForm(FontTextSource* font_text_source, int writing_form) { - (void)font_text_source; - (void)writing_form; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontUnbindRenderer(FontHandle font_handle) { - (void)font_handle; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontWritingGetRenderMetrics(FontWriting* font_writing, FontWritingMetrics* writing_metrics) { - (void)font_writing; - (void)writing_metrics; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontWritingInit(FontWriting* font_writing, FontString font_string, FontRenderCharacter font_character) { - (void)font_writing; - (void)font_string; - (void)font_character; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontWritingLineClear(FontWritingLine writing_line) { - (void)writing_line; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontWritingLineGetOrderingSpace(FontWritingLine writing_line, float* head_space, float* inline_space, float* tail_space, float* advance_space) { - (void)writing_line; - (void)head_space; - (void)inline_space; - (void)tail_space; - (void)advance_space; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontWritingLineGetRenderMetrics(FontWritingLine writing_line, FontWritingMetrics* writing_metrics) { - (void)writing_line; - (void)writing_metrics; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -FontWritingLineStep* APS5_VABI sceFontWritingLineRefersRenderStep(FontWritingLine writing_line) { - (void)writing_line; - NotImplemented_nid_no_patch(__func__); - return nullptr; -} - -int APS5_VABI sceFontWritingLineWritesOrder(FontWritingLine writing_line, uint64_t writing_attribute, const FontWritingMetrics* writing_metrics, void* writing_orderer) { - (void)writing_line; - (void)writing_attribute; - (void)writing_metrics; - (void)writing_orderer; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -const FontWritingStep* APS5_VABI sceFontWritingRefersRenderStep(FontWriting* font_writing) { - (void)font_writing; - NotImplemented_nid_no_patch(__func__); - return nullptr; -} - -FontTextCharacter APS5_VABI sceFontWritingRefersRenderStepCharacter(FontWriting* font_writing, const void** letter_step) { - (void)font_writing; - (void)letter_step; - NotImplemented_nid_no_patch(__func__); - return {}; -} - -int APS5_VABI sceFontWritingSetMaskInvisible(FontWriting* font_writing, int mask) { - (void)font_writing; - (void)mask; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -} diff --git a/core/libs/prx/libSceFont/CMakeLists.txt b/core/libs/prx/libSceFont/CMakeLists.txt new file mode 100644 index 000000000..ccbfe95e4 --- /dev/null +++ b/core/libs/prx/libSceFont/CMakeLists.txt @@ -0,0 +1,2 @@ +include(${CMAKE_CURRENT_SOURCE_DIR}/FontLibrary.cmake) +add_sce_font_library(libSceFont) diff --git a/core/libs/prx/libSceFont/Export.cpp b/core/libs/prx/libSceFont/Export.cpp deleted file mode 100644 index 33f3d0fa4..000000000 --- a/core/libs/prx/libSceFont/Export.cpp +++ /dev/null @@ -1,520 +0,0 @@ -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - -extern "C" { - -int APS5_VABI sceFontAttachDeviceCacheBuffer(FontLibrary library, void* buffer, uint32_t size) { - (void)library; - (void)buffer; - (void)size; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontBindRenderer(FontHandle font_handle, FontRenderer renderer) { - (void)font_handle; - (void)renderer; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontCharacterGetBidiLevel(FontTextCharacter text_character, int* bidi_level) { - (void)text_character; - (void)bidi_level; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontCharacterGetSyllableStringState(FontTextCharacter text_character, int* syllable_string_state) { - (void)text_character; - (void)syllable_string_state; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontCharacterGetTextFontCode(FontTextCharacter text_character, FontHandle* font_handle, uint32_t* text_code) { - (void)text_character; - (void)font_handle; - (void)text_code; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontCharacterGetTextOrder(FontTextCharacter text_character, void** text_order) { - (void)text_character; - (void)text_order; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -uint32_t APS5_VABI sceFontCharacterLooksFormatCharacters(FontTextCharacter text_character) { - (void)text_character; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -uint32_t APS5_VABI sceFontCharacterLooksWhiteSpace(FontTextCharacter text_character) { - (void)text_character; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -FontTextCharacter APS5_VABI sceFontCharacterRefersTextBack(FontTextCharacter text_character) { - (void)text_character; - NotImplemented_nid_no_patch(__func__); - return {}; -} - -FontTextCharacter APS5_VABI sceFontCharacterRefersTextNext(FontTextCharacter text_character) { - (void)text_character; - NotImplemented_nid_no_patch(__func__); - return {}; -} - -FontTextCodes* APS5_VABI sceFontCharactersRefersTextCodes(FontTextCharacter text_character, FontTextCharacter term_character, FontTextCodes* text_codes) { - (void)text_character; - (void)term_character; - (void)text_codes; - NotImplemented_nid_no_patch(__func__); - return nullptr; -} - -int APS5_VABI sceFontCloseFont(FontHandle font_handle) { - (void)font_handle; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontCreateLibrary(const FontMemory* memory, FontLibrarySelection selection, FontLibrary* library) { - (void)memory; - (void)selection; - (void)library; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontCreateLibraryWithEdition(const FontMemory* memory, FontLibrarySelection selection, uint64_t edition, FontLibrary* library) { - (void)memory; - (void)selection; - (void)edition; - (void)library; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontCreateRendererWithEdition(const FontMemory* memory, FontRendererSelection selection, uint64_t edition, FontRenderer* renderer) { - (void)memory; - (void)selection; - (void)edition; - (void)renderer; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontCreateString(const FontMemory* memory, FontTextSource* font_text_source, const FontCreateStringDetail* string_detail, FontString* font_string) { - (void)memory; - (void)font_text_source; - (void)string_detail; - (void)font_string; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontCreateWritingLine(const FontMemory* memory, int writing_form, const void* writing_line_detail, FontWritingLine* writing_line) { - (void)memory; - (void)writing_form; - (void)writing_line_detail; - (void)writing_line; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontDefineAttribute(FontHandle font_handle, int attribute, int* old_attribute) { - (void)font_handle; - (void)attribute; - (void)old_attribute; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontDeleteGlyph(const FontMemory* memory, void** font_glyph) { - (void)memory; - (void)font_glyph; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontDestroyLibrary(FontLibrary* library) { - (void)library; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontDestroyRenderer(FontRenderer* renderer) { - (void)renderer; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontDestroyString(FontString* font_string) { - (void)font_string; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontDestroyWritingLine(FontWritingLine* writing_line) { - (void)writing_line; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGenerateCharGlyph(FontHandle font_handle, uint32_t code, const FontGenerateGlyphDetail* detail, void** font_glyph) { - (void)font_handle; - (void)code; - (void)detail; - (void)font_glyph; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGetCharGlyphMetrics(FontHandle font_handle, uint32_t code, FontGlyphMetrics* metrics) { - (void)font_handle; - (void)code; - (void)metrics; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGetHorizontalLayout(FontHandle font_handle, FontHorizontalLayout* layout) { - (void)font_handle; - (void)layout; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGetRenderCharGlyphMetrics(FontHandle font_handle, uint32_t code, FontGlyphMetrics* metrics) { - (void)font_handle; - (void)code; - (void)metrics; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGetVerticalLayout(FontHandle font_handle, FontVerticalLayout* layout) { - (void)font_handle; - (void)layout; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGlyphDefineAttribute(void* font_glyph, int attribute, int* old_attribute) { - (void)font_glyph; - (void)attribute; - (void)old_attribute; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontMemoryInit(FontMemory* font_memory, void* address, uint32_t size_byte, const FontMemoryInterface* mem_interface, void* mspace_object, FontMemoryDestroyCallback destroy_callback, void* destroy_object) { - (void)font_memory; - (void)address; - (void)size_byte; - (void)mem_interface; - (void)mspace_object; - (void)destroy_callback; - (void)destroy_object; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontMemoryTerm(FontMemory* font_memory) { - (void)font_memory; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontOpenFontInstance(FontHandle font_handle, void* setup_font, FontHandle* out_font_handle) { - (void)font_handle; - (void)setup_font; - (void)out_font_handle; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontOpenFontMemory(FontLibrary library, const void* font_address, uint32_t font_size, const FontOpenDetail* detail, FontHandle* handle) { - (void)library; - (void)font_address; - (void)font_size; - (void)detail; - (void)handle; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontOpenFontSet(FontLibrary library, uint32_t font_set_type, uint32_t open_mode, const FontOpenDetail* detail, FontHandle* handle) { - (void)library; - (void)font_set_type; - (void)open_mode; - (void)detail; - (void)handle; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontRebindRenderer(FontHandle font_handle) { - (void)font_handle; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontRenderCharGlyphImageHorizontal(FontHandle font_handle, uint32_t code, FontRenderSurface* surf, float x, float y, FontGlyphMetrics* metrics, FontRenderResult* result) { - (void)font_handle; - (void)code; - (void)surf; - (void)x; - (void)y; - (void)metrics; - (void)result; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -void APS5_VABI sceFontRenderSurfaceInit(FontRenderSurface* surf, void* buffer, int buf_width_byte, int pixel_size_byte, int width, int height) { - (void)surf; - (void)buffer; - (void)buf_width_byte; - (void)pixel_size_byte; - (void)width; - (void)height; - NotImplemented_nid_no_patch(__func__); -} - -void APS5_VABI sceFontRenderSurfaceSetScissor(FontRenderSurface* surf, uint32_t x0, uint32_t y0, uint32_t x1, uint32_t y1) { - (void)surf; - (void)x0; - (void)y0; - (void)x1; - (void)y1; - NotImplemented_nid_no_patch(__func__); -} - -int APS5_VABI sceFontSetEffectSlant(FontHandle font_handle, float slant_ratio) { - (void)font_handle; - (void)slant_ratio; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontSetEffectWeight(FontHandle font_handle, float weight_x_scale, float weight_y_scale, uint32_t mode) { - (void)font_handle; - (void)weight_x_scale; - (void)weight_y_scale; - (void)mode; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontSetScalePixel(FontHandle font_handle, float w, float h) { - (void)font_handle; - (void)w; - (void)h; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontSetupRenderEffectSlant(FontHandle font_handle, float slant_ratio) { - (void)font_handle; - (void)slant_ratio; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontSetupRenderEffectWeight(FontHandle font_handle, float weight_x_scale, float weight_y_scale, uint32_t mode) { - (void)font_handle; - (void)weight_x_scale; - (void)weight_y_scale; - (void)mode; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontSetupRenderScalePixel(FontHandle font_handle, float w, float h) { - (void)font_handle; - (void)w; - (void)h; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -uint32_t APS5_VABI sceFontStringGetTerminateCode(FontString font_string) { - (void)font_string; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -void* APS5_VABI sceFontStringGetTerminateOrder(FontString font_string) { - (void)font_string; - NotImplemented_nid_no_patch(__func__); - return nullptr; -} - -int APS5_VABI sceFontStringGetWritingForm(FontString font_string) { - (void)font_string; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -FontRenderCharacter APS5_VABI sceFontStringRefersRenderCharacters(FontString font_string, FontTextCharacter start_character, FontTextCharacter last_character, uint32_t* character_count) { - (void)font_string; - (void)start_character; - (void)last_character; - (void)character_count; - NotImplemented_nid_no_patch(__func__); - return {}; -} - -FontTextCharacter APS5_VABI sceFontStringRefersTextCharacters(FontString font_string, uint32_t* character_count) { - (void)font_string; - (void)character_count; - NotImplemented_nid_no_patch(__func__); - return {}; -} - -int APS5_VABI sceFontSupportExternalFonts(FontLibrary library, uint32_t font_max, uint32_t formats) { - (void)library; - (void)font_max; - (void)formats; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontSupportSystemFonts(FontLibrary library) { - (void)library; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -FontTextCodes* APS5_VABI sceFontTextCodesStepBack(FontTextCodes* text_codes_step) { - (void)text_codes_step; - NotImplemented_nid_no_patch(__func__); - return nullptr; -} - -FontTextCodes* APS5_VABI sceFontTextCodesStepNext(FontTextCodes* text_codes_step) { - (void)text_codes_step; - NotImplemented_nid_no_patch(__func__); - return nullptr; -} - -int APS5_VABI sceFontTextSourceInit(FontTextSource* font_text_source, const void* text_address, uint32_t text_size_byte, FontTextParseFunction text_parser, void* text_object) { - (void)font_text_source; - (void)text_address; - (void)text_size_byte; - (void)text_parser; - (void)text_object; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontTextSourceRewind(FontTextSource* font_text_source) { - (void)font_text_source; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontTextSourceSetDefaultFont(FontTextSource* font_text_source, FontHandle default_font) { - (void)font_text_source; - (void)default_font; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontTextSourceSetWritingForm(FontTextSource* font_text_source, int writing_form) { - (void)font_text_source; - (void)writing_form; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontUnbindRenderer(FontHandle font_handle) { - (void)font_handle; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontWritingGetRenderMetrics(FontWriting* font_writing, FontWritingMetrics* writing_metrics) { - (void)font_writing; - (void)writing_metrics; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontWritingInit(FontWriting* font_writing, FontString font_string, FontRenderCharacter font_character) { - (void)font_writing; - (void)font_string; - (void)font_character; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontWritingLineClear(FontWritingLine writing_line) { - (void)writing_line; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontWritingLineGetOrderingSpace(FontWritingLine writing_line, float* head_space, float* inline_space, float* tail_space, float* advance_space) { - (void)writing_line; - (void)head_space; - (void)inline_space; - (void)tail_space; - (void)advance_space; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontWritingLineGetRenderMetrics(FontWritingLine writing_line, FontWritingMetrics* writing_metrics) { - (void)writing_line; - (void)writing_metrics; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -FontWritingLineStep* APS5_VABI sceFontWritingLineRefersRenderStep(FontWritingLine writing_line) { - (void)writing_line; - NotImplemented_nid_no_patch(__func__); - return nullptr; -} - -int APS5_VABI sceFontWritingLineWritesOrder(FontWritingLine writing_line, uint64_t writing_attribute, const FontWritingMetrics* writing_metrics, void* writing_orderer) { - (void)writing_line; - (void)writing_attribute; - (void)writing_metrics; - (void)writing_orderer; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -const FontWritingStep* APS5_VABI sceFontWritingRefersRenderStep(FontWriting* font_writing) { - (void)font_writing; - NotImplemented_nid_no_patch(__func__); - return nullptr; -} - -FontTextCharacter APS5_VABI sceFontWritingRefersRenderStepCharacter(FontWriting* font_writing, const void** letter_step) { - (void)font_writing; - (void)letter_step; - NotImplemented_nid_no_patch(__func__); - return {}; -} - -int APS5_VABI sceFontWritingSetMaskInvisible(FontWriting* font_writing, int mask) { - (void)font_writing; - (void)mask; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -} diff --git a/core/libs/prx/libSceFont/FontLibrary.cmake b/core/libs/prx/libSceFont/FontLibrary.cmake new file mode 100644 index 000000000..36f76410a --- /dev/null +++ b/core/libs/prx/libSceFont/FontLibrary.cmake @@ -0,0 +1,23 @@ +find_package(Freetype REQUIRED) + +function(add_sce_font_library target) + set(fontDir ${CMAKE_CURRENT_FUNCTION_LIST_DIR}) + add_library(${target} SHARED EXCLUDE_FROM_ALL + ${fontDir}/src/Layout.cpp + ${fontDir}/src/Library.cpp + ${fontDir}/src/Open.cpp + ${fontDir}/src/Render.cpp + ${fontDir}/src/State.cpp + ${fontDir}/src/Style.cpp + ${fontDir}/src/Text.cpp + ${fontDir}/src/Unimplemented.cpp + ) + target_include_directories(${target} PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(${target} PRIVATE Freetype::Freetype libc) + set_target_properties(${target} PROPERTIES + CXX_EXTENSIONS OFF + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON + ) + configure_windows_unwind(${target}) +endfunction() diff --git a/core/libs/prx/libSceFont/include/FontDriver.hpp b/core/libs/prx/libSceFont/include/FontDriver.hpp new file mode 100644 index 000000000..e58ae256e --- /dev/null +++ b/core/libs/prx/libSceFont/include/FontDriver.hpp @@ -0,0 +1,360 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef CORE_LIBS_PRX_LIBSCEFONT_INCLUDE_FONTDRIVER_HPP +#define CORE_LIBS_PRX_LIBSCEFONT_INCLUDE_FONTDRIVER_HPP + +#include +#include +#include +#include + +#include "prx/libSceFont/include/FontTypes.hpp" + +namespace Font { + +constexpr std::uint16_t MEMORY_MAGIC = 0x0F00; +constexpr std::uint16_t LIBRARY_MAGIC = 0x0F01; +constexpr std::uint16_t HANDLE_MAGIC = 0x0F02; +constexpr std::uint16_t GLYPH_MAGIC = 0x0F03; +constexpr std::uint16_t RENDERER_MAGIC = 0x0F07; +constexpr std::uint16_t STYLE_FRAME_MAGIC = 0x0F09; + +constexpr std::uint32_t LOCK_BIT = 0x80000000u; +constexpr std::uint32_t OPEN_BIT = 0x40000000u; +constexpr std::uint32_t COUNT_MASK = 0x0FFFFFFFu; + +constexpr std::size_t HORIZONTAL_LAYOUT_SIZE = 0x30; +constexpr std::size_t HORIZONTAL_LINE_ADVANCE = 0x00; +constexpr std::size_t HORIZONTAL_BASELINE = 0x04; +constexpr std::size_t HORIZONTAL_X_BOUND_LO = 0x08; +constexpr std::size_t HORIZONTAL_X_BOUND_HI = 0x0C; +constexpr std::size_t HORIZONTAL_MAX_ADVANCE_WIDTH = 0x10; +constexpr std::size_t HORIZONTAL_CARET_RISE_ADJUST = 0x14; +constexpr std::size_t HORIZONTAL_EFFECT_HEIGHT = 0x18; +constexpr std::size_t HORIZONTAL_HALF_EFFECT_WIDTH = 0x1C; +constexpr std::size_t HORIZONTAL_LEFT_ADJUST = 0x20; + +constexpr std::size_t VERTICAL_LAYOUT_SIZE = 0x20; +constexpr std::size_t VERTICAL_COLUMN_ADVANCE = 0x00; +constexpr std::size_t VERTICAL_BASELINE_OFFSET_X = 0x04; +constexpr std::size_t VERTICAL_METRICS_0X08 = 0x08; +constexpr std::size_t VERTICAL_METRICS_0X0C = 0x0C; +constexpr std::size_t VERTICAL_ADVANCE_HEIGHT = 0x10; +constexpr std::size_t VERTICAL_DECORATION_SPAN = 0x14; +constexpr std::size_t VERTICAL_DECORATION_0X08 = 0x18; +constexpr std::size_t VERTICAL_DECORATION_0X0C = 0x1C; + +inline float LoadFloat(const std::uint8_t* base, std::size_t offset) { + float value; + std::memcpy(&value, base + offset, sizeof(value)); + return value; +} + +inline void StoreFloat(std::uint8_t* base, std::size_t offset, float value) { + std::memcpy(base + offset, &value, sizeof(value)); +} + +struct StyleStateBlock { + std::uint32_t dpi_x; + std::uint32_t dpi_y; + std::uint32_t scale_unit; + std::uint32_t reserved_0x0c; + float scale_w; + float scale_h; + float effect_weight_x; + float effect_weight_y; + float slant_ratio; + float reserved_0x24; +}; + +struct CachedStyle { + StyleStateBlock state; + std::uint32_t reserved_0x28; + std::uint32_t layout_cache_state; + std::uint32_t cache_flags_and_direction; + std::uint32_t cache_lock_word; + std::uint8_t layout_cache_bytes[0x20]; + std::uint32_t reserved_0x58; + std::uint32_t cached_scalar_bits; +}; + +struct FontHandleOpenInfo { + std::uint32_t unique_id_packed; + std::uint32_t ctx_entry_index; + std::uint32_t sub_font_index; + std::uint32_t fontset_flags; + const void* fontset_record; + std::uint64_t reserved_0x18; +}; + +struct FontHandleNative { + std::uint16_t magic; + std::uint16_t flags; + std::uint32_t lock_word; + FontHandleOpenInfo open_info; + FontLibrary library; + void* renderer; + std::uint8_t reserved_0x38[0x08]; + StyleStateBlock style; + CachedStyle cached_style; + std::uint16_t metricA; + std::uint16_t metricB; + std::uint8_t reserved_0xcc[0x1C]; + std::uint8_t reserved_0xe8[0x08]; + FontHandle prevFont; + FontHandle nextFont; +}; + +struct FontObj { + std::uint32_t refcount; + std::uint32_t reserved_0x04; + std::uint32_t reserved_0x08; + std::uint32_t sub_font_index; + FontObj* prev; + FontObj* next; + std::uint8_t reserved_0x20[0x08]; + void* open_ctx_0x28; + void* ft_face; + FontHandle font_handle; + std::int32_t shift_units_x; + std::int32_t shift_units_y; + std::uint64_t layout_seed_pair; + float scale_x_0x50; + float scale_y_0x54; + void* ft_ctx_0x58; + std::uint8_t reserved_0x60[0x04]; + std::int32_t cached_glyph_index_0x64; + std::uint64_t cached_units_x_0x68; + std::uint64_t cached_units_y_0x70; + std::int64_t shift_cache_x; + std::int64_t shift_cache_y; + std::array layout_seed_vec; + std::array layout_scale_vec; + std::uint8_t reserved_0xA8[0x130 - 0xA8]; + std::uint32_t glyph_cfg_word_0x130; + std::uint8_t glyph_cfg_mode_0x134; + std::uint8_t glyph_cfg_byte_0x135; + std::uint8_t glyph_cfg_byte_0x136; + std::uint8_t reserved_0x137[0x200 - 0x137]; +}; + +struct FontCtxHeader { + std::uint32_t lock_word; + std::uint32_t max_entries; + void* base; + std::uint8_t reserved_0x10[0x10]; +}; + +struct FontCtxEntry { + std::uint32_t reserved_0x00; + std::uint32_t active; + std::uint64_t font_address; + std::uint32_t unique_id; + std::uint32_t lock_mode1; + std::uint32_t lock_mode3; + std::uint32_t lock_mode2; + void* obj_mode1; + void* obj_mode3; + void* obj_mode2; + std::uint8_t reserved_0x38[0x08]; +}; + +struct SysDriver; + +struct FontLibNative { + std::uint16_t magic; + std::uint16_t reserved0; + std::uint32_t lock_word; + std::uint32_t flags; + std::uint32_t feature_word; + std::uint32_t mem_kind; + std::uint32_t region_size; + void* region_base; + void* alloc_ctx; + const FontMemoryInterface* iface; + std::uint8_t reserved_0x30[0x20]; + FontAllocFunction alloc_fn; + FontFreeFunction dealloc_fn; + FontReallocFunction realloc_fn; + FontCallocFunction calloc_fn; + std::uint8_t reserved_0x70[0x10]; + const SysDriver* sys_driver; + void* fontset_registry; + std::uint8_t reserved_0x90[0x10]; + void* sysfonts_ctx; + void* external_fonts_ctx; + std::uint32_t* device_cache_buf; + std::uint8_t reserved_0xb8[0x04]; + std::uint32_t workspace_size; + void* workspace; + void* sysfont_desc_ptr; + std::uint64_t sysfont_flags; + FontHandle* list_head_ptr; + FontHandle list_head; + std::uint8_t reserved_0xe8[0x18]; +}; + +using DriverPixelResolutionFunction = std::uint32_t (APS5_VABI*)(); +using DriverInitFunction = int (APS5_VABI*)(const FontMemory* memory, FontLibNative* library); +using DriverTermFunction = int (APS5_VABI*)(FontLibNative* library); +using DriverSupportFunction = int (APS5_VABI*)(FontLibNative* library, std::uint32_t formats); +using DriverOpenFunction = int (APS5_VABI*)(FontLibNative* library, std::uint32_t mode, const void* fontAddress, std::uint32_t fontSize, std::uint32_t subFontIndex, std::uint32_t uniqueWord, FontObj** inoutFontObj); +using DriverCloseFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t flags); +using DriverScaleFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint16_t* outUnitsPerEm, float* outScale); +using DriverMetricFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t metricId, std::uint16_t* outMetric); +using DriverGlyphsCountFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t* outCount); +using DriverGlyphIndexFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t codepoint, std::uint32_t* outGlyphIndex); +using DriverSetCharWithDpiFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t dpiX, std::uint32_t dpiY, float scaleX, float scaleY, float* outScaleX, float* outScaleY); +using DriverSetCharDefaultDpiFunction = int (APS5_VABI*)(FontObj* fontObj, float scaleX, float scaleY, float* outScaleX, float* outScaleY); +using DriverComputeLayoutFunction = int (APS5_VABI*)(FontObj* fontObj, const StyleStateBlock* style, std::uint8_t* outWords); +using DriverLoadGlyphCachedFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t glyphIndex, std::int32_t mode, std::uint64_t* outWords); +using DriverGetGlyphMetricsFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t* optParam2, std::uint8_t mode, std::uint8_t* outParams, FontGlyphMetrics* outMetrics); +using DriverApplyGlyphAdjustFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t p2, std::uint32_t glyphIndex, std::int32_t p4, std::int32_t p5, std::uint32_t* inoutGlyphIndex); +using DriverConfigureGlyphFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t* inParams, std::int32_t mode, std::uint32_t* inoutState); + +struct SysDriver { + std::uint32_t magic; + std::uint32_t reserved; + void* reserved_ptr1; + DriverPixelResolutionFunction pixel_resolution; + DriverInitFunction init; + DriverTermFunction term; + DriverSupportFunction support_formats; + std::uint8_t reserved_0x30[0x08]; + DriverOpenFunction open; + DriverCloseFunction close; + std::uint8_t reserved_0x48[0x08]; + DriverScaleFunction scale; + std::uint8_t reserved_0x58[0x08]; + DriverMetricFunction metric; + DriverGlyphsCountFunction glyphs_count; + std::uint8_t reserved_0x70[0x08]; + DriverGlyphIndexFunction glyph_index; + DriverSetCharWithDpiFunction set_char_with_dpi; + DriverSetCharDefaultDpiFunction set_char_default_dpi; + std::uint8_t reserved_0x90[0x10]; + DriverComputeLayoutFunction compute_layout; + DriverLoadGlyphCachedFunction load_glyph_cached; + std::uint8_t reserved_0xb0[0x08]; + DriverGetGlyphMetricsFunction get_glyph_metrics; + std::uint8_t reserved_0xc0[0x20]; + DriverApplyGlyphAdjustFunction apply_glyph_adjust; + std::uint8_t reserved_0xe8[0x20]; + DriverConfigureGlyphFunction configure_glyph; + std::uint8_t reserved_0x110[0x08]; + DriverComputeLayoutFunction compute_layout_alt; +}; + +struct RendererNative { + std::uint16_t magic; + std::uint16_t reserved02; + std::uint8_t reserved_04[0x08]; + std::uint8_t feature_byte_0x0c; + std::uint8_t reserved_0d[0x03]; + std::uint32_t mem_kind; + std::uint32_t region_size; + void* region_base; + void* alloc_ctx; + const FontMemoryInterface* mem_iface; + std::uint8_t reserved_30[0x20]; + FontAllocFunction alloc_fn; + FontFreeFunction free_fn; + FontReallocFunction realloc_fn; + FontCallocFunction calloc_fn; + std::uint8_t reserved_70[0x10]; + void* selection; + std::uint8_t reserved_88[0x08]; + std::uint64_t outline_magic_0x90; + void* workspace; + std::uint64_t workspace_size; + std::uint32_t reserved_a8; + std::uint32_t outline_policy_flag; +}; + +struct RendererFtBackend { + void* renderer_header_0x10; + std::uintptr_t unknown_0x08; + std::uintptr_t unknown_0x10; + std::uintptr_t unknown_0x18; + void* initialized_marker; + std::uint8_t reserved_0x28[0x20]; +}; + +struct RendererFt { + RendererNative base; + std::uint8_t reserved_0x0B0[0x70]; + RendererFtBackend ft_backend; +}; + +using RendererCreateFunction = int (APS5_VABI*)(RendererNative* renderer); +using RendererDestroyFunction = int (APS5_VABI*)(RendererNative* renderer); +using RendererQueryFunction = std::uint64_t (APS5_VABI*)(RendererNative* renderer, std::uint8_t* params, std::int64_t* outPtr, std::uint8_t* outVector); + +struct RendererSelection { + std::uint32_t magic; + std::uint32_t size; + RendererCreateFunction create_fn; + RendererDestroyFunction destroy_fn; + RendererQueryFunction query_fn; +}; + +static_assert(sizeof(StyleStateBlock) == 0x28); +static_assert(offsetof(StyleStateBlock, scale_w) == 0x10); +static_assert(offsetof(StyleStateBlock, slant_ratio) == 0x20); +static_assert(sizeof(CachedStyle) == 0x60); +static_assert(offsetof(CachedStyle, layout_cache_state) == 0x2C); +static_assert(offsetof(CachedStyle, cache_lock_word) == 0x34); +static_assert(offsetof(CachedStyle, layout_cache_bytes) == 0x38); +static_assert(offsetof(CachedStyle, cached_scalar_bits) == 0x5C); +static_assert(sizeof(FontHandleOpenInfo) == 0x20); +static_assert(sizeof(FontHandleNative) == 0x100); +static_assert(offsetof(FontHandleNative, open_info) == 0x08); +static_assert(offsetof(FontHandleNative, library) == 0x28); +static_assert(offsetof(FontHandleNative, renderer) == 0x30); +static_assert(offsetof(FontHandleNative, style) == 0x40); +static_assert(offsetof(FontHandleNative, cached_style) == 0x68); +static_assert(offsetof(FontHandleNative, metricA) == 0xC8); +static_assert(offsetof(FontHandleNative, prevFont) == 0xF0); +static_assert(sizeof(FontObj) == 0x200); +static_assert(offsetof(FontObj, ft_face) == 0x30); +static_assert(offsetof(FontObj, shift_cache_x) == 0x78); +static_assert(offsetof(FontObj, layout_scale_vec) == 0x98); +static_assert(offsetof(FontObj, glyph_cfg_word_0x130) == 0x130); +static_assert(sizeof(FontCtxHeader) == 0x20); +static_assert(sizeof(FontCtxEntry) == 0x40); +static_assert(offsetof(FontCtxEntry, obj_mode2) == 0x30); +static_assert(sizeof(FontLibNative) == 0x100); +static_assert(offsetof(FontLibNative, alloc_ctx) == 0x20); +static_assert(offsetof(FontLibNative, iface) == 0x28); +static_assert(offsetof(FontLibNative, alloc_fn) == 0x50); +static_assert(offsetof(FontLibNative, sys_driver) == 0x80); +static_assert(offsetof(FontLibNative, fontset_registry) == 0x88); +static_assert(offsetof(FontLibNative, sysfonts_ctx) == 0xA0); +static_assert(offsetof(FontLibNative, external_fonts_ctx) == 0xA8); +static_assert(offsetof(FontLibNative, device_cache_buf) == 0xB0); +static_assert(offsetof(FontLibNative, workspace) == 0xC0); +static_assert(offsetof(FontLibNative, sysfont_flags) == 0xD0); +static_assert(offsetof(FontLibNative, list_head_ptr) == 0xD8); +static_assert(offsetof(FontLibNative, list_head) == 0xE0); +static_assert(sizeof(SysDriver) == 0x120); +static_assert(offsetof(SysDriver, pixel_resolution) == 0x10); +static_assert(offsetof(SysDriver, open) == 0x38); +static_assert(offsetof(SysDriver, scale) == 0x50); +static_assert(offsetof(SysDriver, glyph_index) == 0x78); +static_assert(offsetof(SysDriver, compute_layout) == 0xA0); +static_assert(offsetof(SysDriver, get_glyph_metrics) == 0xB8); +static_assert(offsetof(SysDriver, apply_glyph_adjust) == 0xE0); +static_assert(offsetof(SysDriver, configure_glyph) == 0x108); +static_assert(offsetof(SysDriver, compute_layout_alt) == 0x118); +static_assert(sizeof(RendererNative) == 0xB0); +static_assert(offsetof(RendererNative, alloc_fn) == 0x50); +static_assert(offsetof(RendererNative, selection) == 0x80); +static_assert(offsetof(RendererNative, outline_policy_flag) == 0xAC); +static_assert(sizeof(RendererFtBackend) == 0x48); +static_assert(sizeof(RendererFt) == 0x168); +static_assert(offsetof(RendererFt, ft_backend) == 0x120); + +} + +#endif diff --git a/core/libs/prx/libSceFont/include/FontFreeType.hpp b/core/libs/prx/libSceFont/include/FontFreeType.hpp new file mode 100644 index 000000000..eeef92b9f --- /dev/null +++ b/core/libs/prx/libSceFont/include/FontFreeType.hpp @@ -0,0 +1,151 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef CORE_LIBS_PRX_LIBSCEFONT_INCLUDE_FONTFREETYPE_HPP +#define CORE_LIBS_PRX_LIBSCEFONT_INCLUDE_FONTFREETYPE_HPP + +#include +#include +#include +#include +#include + +#include +#include FT_FREETYPE_H + +namespace Font { + +constexpr float ONE_OVER_64 = 1.0f / 64.0f; + +inline std::int32_t TruncateFloatToInt(float value) { + if (!std::isfinite(value) || value > 2147483647.0f || value < -2147483648.0f) return std::numeric_limits::min(); + return static_cast(value); +} + +inline std::int32_t RoundMulFixed(std::int64_t value, std::int64_t fixed16) { + const std::int64_t product = value * fixed16; + const std::int64_t signAdjust = ((~value) >> 63) * -0x10000LL; + const std::int64_t base = signAdjust + product; + std::int64_t rounded = base - 0x8000LL; + if (rounded < 0) rounded = base + 0x7FFFLL; + return static_cast(static_cast(rounded) >> 16); +} + +inline std::int32_t TruncFixed(std::int64_t fixed16) { + std::int64_t value = fixed16; + if (fixed16 < 0) value = fixed16 + 0xFFFFLL; + return static_cast(value >> 16); +} + +inline std::int64_t TruncMulUnits(std::int64_t fixed16, std::uint16_t units) { + const std::int64_t product = fixed16 * static_cast(units); + std::int64_t rounded = product; + if (product < 0) rounded = product + 0xFFFFLL; + return static_cast(static_cast(rounded >> 16)); +} + +inline float FixedMulUnitsToF26Dot6(std::int64_t fixed16, std::uint16_t unitsPerEm) { + const std::int64_t product = fixed16 * static_cast(unitsPerEm); + std::int64_t rounded = product + 0xFFFF; + if (product >= 0) rounded = product; + return static_cast(rounded >> 16) * ONE_OVER_64; +} + +inline int FloorToInt(float value) { + int result = static_cast(std::trunc(value)); + if (static_cast(result) > value) --result; + return result; +} + +inline int CeilToInt(float value) { + int result = static_cast(std::trunc(value)); + if (static_cast(result) < value) ++result; + return result; +} + +inline FT_UInt ResolveGlyphIndexWithFallback(FT_Face face, std::uint32_t codepoint) { + if (!face) return 0; + const auto code = static_cast(codepoint); + const FT_CharMap original = face->charmap; + FT_UInt glyphIndex = FT_Get_Char_Index(face, code); + if (glyphIndex != 0 || !face->charmaps || face->num_charmaps <= 1) return glyphIndex; + for (int i = 0; i < face->num_charmaps; ++i) { + const FT_CharMap charmap = face->charmaps[i]; + if (!charmap || charmap == original) continue; + if (FT_Set_Charmap(face, charmap) != 0) continue; + glyphIndex = FT_Get_Char_Index(face, code); + if (glyphIndex != 0) break; + } + if (original && face->charmap != original) FT_Set_Charmap(face, original); + return glyphIndex; +} + +inline FT_F26Dot6 ClampPpem(FT_F26Dot6 value) { + constexpr FT_F26Dot6 maxPpem = static_cast(65535 * 64); + return std::clamp(value, static_cast(-maxPpem), maxPpem); +} + +inline FT_Error SetCharSizeCompat(FT_Face face, FT_F26Dot6 charW, FT_F26Dot6 charH, std::uint32_t dpiX, std::uint32_t dpiY, FT_F26Dot6* usedW = nullptr, FT_F26Dot6* usedH = nullptr) { + const auto writeUsed = [&](FT_F26Dot6 w, FT_F26Dot6 h) { + if (usedW) *usedW = w; + if (usedH) *usedH = h; + }; + if (!face) { + writeUsed(charW, charH); + return FT_Err_Invalid_Face_Handle; + } + FT_Error error = FT_Set_Char_Size(face, charW, charH, dpiX, dpiY); + if (error == 0) { + writeUsed(charW, charH); + return 0; + } + const FT_F26Dot6 clampedW = ClampPpem(charW); + const FT_F26Dot6 clampedH = ClampPpem(charH); + if (clampedW != charW || clampedH != charH) { + error = FT_Set_Char_Size(face, clampedW, clampedH, dpiX, dpiY); + if (error == 0) { + writeUsed(clampedW, clampedH); + return 0; + } + } + if (face->num_fixed_sizes <= 0 || !face->available_sizes) { + writeUsed(charW, charH); + return error; + } + const double requestW = static_cast(std::llabs(static_cast(charW))) / 64.0; + const double requestH = static_cast(std::llabs(static_cast(charH))) / 64.0; + const double targetW = requestW > 0.0 ? requestW : requestH; + const double targetH = requestH > 0.0 ? requestH : requestW; + if (targetW <= 0.0 || targetH <= 0.0) { + writeUsed(charW, charH); + return error; + } + int bestIndex = -1; + double bestScore = std::numeric_limits::infinity(); + for (int i = 0; i < face->num_fixed_sizes; ++i) { + const FT_Bitmap_Size& strike = face->available_sizes[i]; + const double strikeW = std::max(1.0, static_cast(strike.x_ppem) / 64.0); + const double strikeH = std::max(1.0, static_cast(strike.y_ppem) / 64.0); + const double score = std::abs(strikeW - targetW) / targetW + std::abs(strikeH - targetH) / targetH; + if (score < bestScore) { + bestScore = score; + bestIndex = i; + } + } + if (bestIndex < 0) { + writeUsed(charW, charH); + return error; + } + error = FT_Select_Size(face, bestIndex); + if (error == 0) { + const FT_Bitmap_Size& strike = face->available_sizes[bestIndex]; + writeUsed(strike.x_ppem, strike.y_ppem); + return 0; + } + writeUsed(charW, charH); + return error; +} + +} + +#endif diff --git a/core/libs/prx/libSceFont/include/FontInternal.hpp b/core/libs/prx/libSceFont/include/FontInternal.hpp new file mode 100644 index 000000000..426a21bee --- /dev/null +++ b/core/libs/prx/libSceFont/include/FontInternal.hpp @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef CORE_LIBS_PRX_LIBSCEFONT_INCLUDE_FONTINTERNAL_HPP +#define CORE_LIBS_PRX_LIBSCEFONT_INCLUDE_FONTINTERNAL_HPP + +#include +#include +#include + +#include +#include FT_FREETYPE_H + +#include "prx/libSceFont/include/FontDriver.hpp" + +namespace Font { + +constexpr std::uint8_t STYLE_FRAME_FLAG_SCALE = 0x01; +constexpr std::uint8_t STYLE_FRAME_FLAG_SLANT = 0x02; +constexpr std::uint8_t STYLE_FRAME_FLAG_WEIGHT = 0x04; +constexpr float POINTS_PER_INCH = 72.0f; + +struct FontState { + std::vector faceData; + FT_Face face = nullptr; + float scaleW = 16.0f; + float scaleH = 16.0f; + + FontState() = default; + FontState(const FontState&) = delete; + FontState& operator=(const FontState&) = delete; + ~FontState(); +}; + +struct GeneratedGlyph { + FontGlyphOpaque glyph{}; + FontGlyphMetrics metrics{}; + FontGlyphMetricsHorizontal metricsHorizontal{}; + FontGlyphMetricsHorizontalX metricsHorizontalX{}; + FontGlyphMetricsHorizontalAdvance metricsHorizontalAdvance{}; + std::uint32_t codepoint = 0; + FontHandle owner = nullptr; + FontGlyphOutline outline{}; + std::vector outlinePoints; + std::vector outlineTags; + std::vector outlineContours; + bool metricsInitialized = false; + bool outlineInitialized = false; +}; + +struct RenderSurfaceSystemUse { + FontStyleFrame* styleframe; + float catchedScale; +}; + +void Backoff(); +std::uint32_t AcquireWordLock(std::uint32_t& word); +FontHandleNative* GetNativeFont(FontHandle handle); +bool AcquireLibraryLock(FontLibNative* library, std::uint32_t& previous); +void ReleaseLibraryLock(FontLibNative* library, std::uint32_t previous); +bool AcquireFontLock(FontHandleNative* font, std::uint32_t& previous); +void ReleaseFontLock(FontHandleNative* font, std::uint32_t previous); +bool AcquireCachedStyleLock(FontHandleNative* font, std::uint32_t& previous); +void ReleaseCachedStyleLock(FontHandleNative* font, std::uint32_t previous); +void LinkFontToLibrary(FontLibNative* library, FontHandle handle); +void UnlinkFontFromLibrary(FontLibNative* library, FontHandle handle); + +std::uint32_t* EntryLockWord(FontCtxEntry* entry, std::uint32_t modeLow); +void** EntryObjectSlot(FontCtxEntry* entry, std::uint32_t modeLow); +FontCtxEntry* AcquireFontCtxEntry(void* ctx, std::uint32_t index, std::uint32_t modeLow, FontObj** outObject, std::uint32_t* outLockWord); +void ReleaseFontCtxEntryLock(FontCtxEntry* entry, std::uint32_t modeLow, std::uint32_t lockWord); +FontObj* FindSubFont(FontObj* head, std::uint32_t subFontIndex); +void ReleaseFontObjectsForHandle(FontHandleNative* font); + +FontState* TryGetState(FontHandle handle); +FontState& ResetState(FontHandle handle); +void RemoveState(FontHandle handle); +void LoadStateFace(FontState& state, std::vector bytes, std::uint32_t subFontIndex); + +void TrackGeneratedGlyph(FontGlyph glyph); +bool ForgetGeneratedGlyph(FontGlyph glyph); +GeneratedGlyph* TryGetGeneratedGlyph(FontGlyph glyph); +void PopulateGlyphMetricVariants(GeneratedGlyph& glyph); +void BuildBoundingOutline(GeneratedGlyph& glyph); +bool BuildTrueOutline(GeneratedGlyph& glyph); + +int StyleStateSetScalePixel(StyleStateBlock* style, float w, float h); +int StyleStateSetScalePoint(StyleStateBlock* style, float w, float h); +int StyleStateGetScalePixel(const StyleStateBlock* style, float* w, float* h); +int StyleStateGetScalePoint(const StyleStateBlock* style, float* w, float* h); +int StyleStateSetDpi(StyleStateBlock* style, std::uint32_t hDpi, std::uint32_t vDpi); +int StyleStateSetSlantRatio(StyleStateBlock* style, float slantRatio); +int StyleStateGetSlantRatio(const StyleStateBlock* style, float* slantRatio); +int StyleStateSetWeightScale(StyleStateBlock* style, float weightXScale, float weightYScale); +int StyleStateGetWeightScale(const StyleStateBlock* style, float* weightXScale, float* weightYScale, std::uint32_t* mode); +bool ValidStyleFrame(const FontStyleFrame* frame); + +std::uint8_t CachedStyleCacheFlags(const CachedStyle& style); +void CachedStyleSetCacheFlags(CachedStyle& style, std::uint8_t flags); +void CachedStyleSetDirectionWord(CachedStyle& style, std::uint16_t word); +float CachedStyleGetScalar(const CachedStyle& style); +void CachedStyleSetScalar(CachedStyle& style, float value); + +void ClearRenderOutputs(FontGlyphMetrics* metrics, FontRenderOutput* result); +int ComputeHorizontalLayout(FontHandle handle, const StyleStateBlock* style, std::uint8_t* outWords); +int ComputeVerticalLayout(FontHandle handle, const StyleStateBlock* style, std::uint8_t* outWords); +int GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMetrics* metrics, bool useCachedStyle); +int RenderCharGlyphImageCore(FontHandle handle, std::uint32_t code, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result); + +} + +#endif diff --git a/core/libs/prx/libSceFont/include/FontTypes.hpp b/core/libs/prx/libSceFont/include/FontTypes.hpp new file mode 100644 index 000000000..d65cb4b6c --- /dev/null +++ b/core/libs/prx/libSceFont/include/FontTypes.hpp @@ -0,0 +1,432 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef CORE_LIBS_PRX_LIBSCEFONT_INCLUDE_FONTTYPES_HPP +#define CORE_LIBS_PRX_LIBSCEFONT_INCLUDE_FONTTYPES_HPP + +#include +#include + +#include "prx/libc/include/General.hpp" + +constexpr int SCE_FONT_OK = 0; +constexpr int SCE_FONT_ERROR_FATAL = static_cast(0x80460001); +constexpr int SCE_FONT_ERROR_INVALID_PARAMETER = static_cast(0x80460002); +constexpr int SCE_FONT_ERROR_INVALID_MEMORY = static_cast(0x80460003); +constexpr int SCE_FONT_ERROR_INVALID_LIBRARY = static_cast(0x80460004); +constexpr int SCE_FONT_ERROR_INVALID_FONT_HANDLE = static_cast(0x80460005); +constexpr int SCE_FONT_ERROR_INVALID_GLYPH = static_cast(0x80460006); +constexpr int SCE_FONT_ERROR_INVALID_RENDERER = static_cast(0x80460007); +constexpr int SCE_FONT_ERROR_INVALID_TEXT_SOURCE = static_cast(0x80460008); +constexpr int SCE_FONT_ERROR_INVALID_STRING = static_cast(0x80460009); +constexpr int SCE_FONT_ERROR_INVALID_WRITING = static_cast(0x8046000A); +constexpr int SCE_FONT_ERROR_INVALID_WORDS = static_cast(0x8046000B); +constexpr int SCE_FONT_ERROR_ALLOCATION_FAILED = static_cast(0x80460010); +constexpr int SCE_FONT_ERROR_FS_OPEN_FAILED = static_cast(0x80460011); +constexpr int SCE_FONT_ERROR_NO_SUPPORT_LIBRARY = static_cast(0x80460018); +constexpr int SCE_FONT_ERROR_NO_SUPPORT_FORMAT = static_cast(0x80460019); +constexpr int SCE_FONT_ERROR_NO_SUPPORT_FUNCTION = static_cast(0x80460020); +constexpr int SCE_FONT_ERROR_ALREADY_SPECIFIED = static_cast(0x80460021); +constexpr int SCE_FONT_ERROR_ALREADY_ATTACHED = static_cast(0x80460022); +constexpr int SCE_FONT_ERROR_ALREADY_OPENED = static_cast(0x80460023); +constexpr int SCE_FONT_ERROR_NOT_ATTACHED_CACHE_BUFFER = static_cast(0x80460025); +constexpr int SCE_FONT_ERROR_NO_SUPPORT_FONTSET = static_cast(0x80460031); +constexpr int SCE_FONT_ERROR_FONT_OPEN_MAX = static_cast(0x80460033); +constexpr int SCE_FONT_ERROR_FONT_OPEN_FAILED = static_cast(0x80460036); +constexpr int SCE_FONT_ERROR_FONT_CLOSE_FAILED = static_cast(0x80460037); +constexpr int SCE_FONT_ERROR_NO_SUPPORT_TYPOGRAPHY = static_cast(0x80460040); +constexpr int SCE_FONT_ERROR_NO_SUPPORT_CODE = static_cast(0x80460041); +constexpr int SCE_FONT_ERROR_NO_SUPPORT_GLYPH = static_cast(0x80460042); +constexpr int SCE_FONT_ERROR_NO_SUPPORT_SCRIPT = static_cast(0x80460043); +constexpr int SCE_FONT_ERROR_NO_SUPPORT_LANGUAGE = static_cast(0x80460044); +constexpr int SCE_FONT_ERROR_NO_SUPPORT_SURFACE = static_cast(0x80460050); +constexpr int SCE_FONT_ERROR_UNSET_PARAMETER = static_cast(0x80460058); +constexpr int SCE_FONT_ERROR_FUNCTIONAL_LIMIT = static_cast(0x8046005C); +constexpr int SCE_FONT_ERROR_ALREADY_BOUND_RENDERER = static_cast(0x80460060); +constexpr int SCE_FONT_ERROR_NOT_BOUND_RENDERER = static_cast(0x80460061); +constexpr int SCE_FONT_ERROR_RENDERER_ALLOCATION_FAILED = static_cast(0x80460063); +constexpr int SCE_FONT_ERROR_RENDERER_ALLOCATION_LIMITED = static_cast(0x80460064); +constexpr int SCE_FONT_ERROR_RENDERER_RENDER_FAILED = static_cast(0x80460065); + +struct FontHandleOpaque { + std::uint32_t reserved[64]; +}; + +using FontHandle = FontHandleOpaque*; +using FontLibrary = void*; +using FontRenderer = void*; + +struct FontMemory; + +using FontAllocFunction = void* (APS5_VABI*)(void* object, std::uint32_t size); +using FontFreeFunction = void (APS5_VABI*)(void* object, void* p); +using FontReallocFunction = void* (APS5_VABI*)(void* object, void* p, std::uint32_t newSize); +using FontCallocFunction = void* (APS5_VABI*)(void* object, std::uint32_t nBlock, std::uint32_t size); +using FontMspaceCreateFunction = void* (APS5_VABI*)(void* parent, const char* name, void* address, std::uint32_t size, std::uint32_t attr); +using FontMspaceDestroyFunction = void (APS5_VABI*)(void* parent, void* mspace); +using FontMemoryDestroyFunction = void (APS5_VABI*)(FontMemory* fontMemory, void* object, void* destroyArg); + +struct FontMemoryInterface { + FontAllocFunction alloc; + FontFreeFunction dealloc; + FontReallocFunction realloc_fn; + FontCallocFunction calloc_fn; + FontMspaceCreateFunction mspace_create; + FontMspaceDestroyFunction mspace_destroy; +}; + +struct FontMemory { + std::uint16_t mem_kind; + std::uint16_t attr_bits; + std::uint32_t region_size; + void* region_base; + void* mspace_handle; + const FontMemoryInterface* iface; + FontMemoryDestroyFunction on_destroy; + void* destroy_ctx; + void* some_ctx1; + void* some_ctx2; +}; + +struct FontOpenDetail { + std::uint16_t tag; + std::uint16_t pad16; + std::uint32_t flags; + std::uint32_t subfont_index; + std::int32_t unique_id; + const void* reserved_ptr2; + const void* reserved_ptr1; +}; + +struct FontGlyphMetrics { + float width; + float height; + struct { + float bearingX; + float bearingY; + float advance; + } Horizontal; + struct { + float bearingX; + float bearingY; + float advance; + } Vertical; +}; + +struct FontGlyphMetricsHorizontal { + float width; + float height; + struct { + float bearing_x; + float bearing_y; + float advance; + } horizontal; +}; + +struct FontGlyphMetricsHorizontalX { + float width; + struct { + float bearing_x; + float advance; + } horizontal; +}; + +struct FontGlyphMetricsHorizontalAdvance { + struct { + float advance; + } horizontal; +}; + +struct FontKerning { + float offsetX; + float offsetY; + float positionX; + float positionY; +}; + +struct FontGlyphImageMetrics { + float bearingX; + float bearingY; + float advance; + float stride; + std::uint32_t width; + std::uint32_t height; +}; + +struct FontGenerateGlyphDetail { + std::uint16_t id; + std::uint16_t res0; + std::uint16_t form_options; + std::uint8_t glyph_form; + std::uint8_t metrics_form; + const FontMemory* mem; + void* res1; + void* res2; +}; + +struct FontGlyphOutlinePoint { + float x; + float y; +}; + +struct FontGlyphOutline { + std::int16_t contours_cnt; + std::int16_t points_cnt; + std::uint32_t outline_flags; + FontGlyphOutlinePoint* points_ptr; + std::uint8_t* tags_ptr; + std::uint16_t* contour_end_idx; +}; + +struct FontGlyphOpaque { + std::uint16_t magic; + std::uint16_t flags; + std::uint8_t glyph_form; + std::uint8_t metrics_form; + std::uint16_t em_size; + std::uint16_t baseline; + std::uint16_t height_px; + std::uint16_t origin_x; + std::uint16_t origin_y; + float scale_x; + float base_scale; + const FontMemory* memory; +}; + +using FontGlyph = FontGlyphOpaque*; + +struct FontResultStage { + std::uint8_t* p_00; + std::uint32_t u32_08; + std::uint32_t u32_0C; + std::uint32_t u32_10; +}; + +struct FontSurfaceImage { + std::uint8_t* address; + std::uint32_t widthByte; + std::uint8_t pixelSizeByte; + std::uint8_t pixelFormat; + std::uint16_t pad16; +}; + +struct FontRenderOutput { + const FontResultStage* stage; + FontSurfaceImage SurfaceImage; + struct { + std::uint32_t x; + std::uint32_t y; + std::uint32_t w; + std::uint32_t h; + } UpdateRect; + FontGlyphImageMetrics ImageMetrics; +}; + +struct FontTextCharacter { + FontTextCharacter* prev; + FontTextCharacter* next; + void* textOrder; + FontHandle font; + void* shapeEntry; + std::uint32_t characterCode; + std::uint8_t reserved_0x2c[4]; + std::uint8_t clusterSpan; + std::uint8_t clusterIndex; + std::uint8_t clusterKind; + std::int8_t synthetic; + std::uint8_t reserved_0x34[4]; + std::uint64_t flags; + std::uint8_t reserved_0x40[136]; +}; + +struct FontRenderSurface { + void* buffer; + std::int32_t widthByte; + std::int8_t pixelSizeByte; + std::uint8_t pad0; + std::uint8_t styleFlag; + std::uint8_t pad2; + std::int32_t width; + std::int32_t height; + std::uint32_t sc_x0; + std::uint32_t sc_y0; + std::uint32_t sc_x1; + std::uint32_t sc_y1; + std::uint64_t reserved_q[11]; +}; + +struct FontStyleFrame { + std::uint16_t magic; + std::uint8_t flags1; + std::uint8_t flags2; + std::uint32_t hDpi; + std::uint32_t vDpi; + std::uint32_t scaleUnit; + float baseScale; + float scalePixelW; + float scalePixelH; + float effectWeightX; + float effectWeightY; + float slantRatio; + std::uint32_t reserved_0x28; + std::uint32_t layout_cache_state; + std::uint32_t cache_flags_and_direction; + std::uint32_t cache_lock_word; + std::uint8_t layout_cache_bytes[0x20]; + std::uint32_t reserved_0x58; + std::uint32_t cached_scalar_bits; +}; + +struct FontTextSource; + +union FontTextParseResult { + void* reserved[4]; + struct { + FontHandle font; + std::uint32_t code; + } FontCode; + struct { + void* reserved; + std::uint32_t terminateCode; + } Terminate; + struct { + void* reserved; + std::int32_t errorCode; + } Error; +}; + +using FontTextParseFunction = std::int32_t (APS5_VABI*)(FontTextSource* fontTextSource, void** order, FontTextParseResult* result); + +struct FontTextSource { + std::uint64_t systemUse0; + const void* start; + const void* end; + const void* current; + FontTextParseFunction textParser; + void* textObject; + FontHandle defaultFont; + void* systemUse[5]; +}; + +struct FontCreateStringDetail { + std::uint16_t detailId; + std::uint8_t detailType; + std::uint8_t detections; + std::uint32_t ordersOption; + FontHandle defaultFont; + union { + struct { + void* function; + void* object; + } Callback; + } Orders; +}; + +struct FontStringOpaque; +using FontString = FontStringOpaque*; + +struct FontWriting { + void* systemUse[32]; +}; + +struct FontWritingProfile { + std::uint32_t characterCount : 8; + std::uint32_t invisibleGlyph : 1; + std::uint32_t reserved : 23; +}; + +struct FontWritingStep { + float x; + float y; + float advanceX; + float advanceY; + FontHandle font; + FontWritingProfile Profile; + std::uint32_t glyphCode; + struct { + float x; + float y; + } Positioning; + FontGlyphMetrics GlyphMetrics; +}; + +struct FontWritingLetterStep { + float x; + float y; + float advanceX; + float advanceY; + struct { + std::uint32_t textsCount; + std::uint32_t textsIndex; + std::uint32_t glyphsCount; + std::uint32_t glyphsIndex; + std::uint8_t baseComponentsCount; + std::uint8_t baseComponentsIndex; + std::uint8_t baseFocusCount; + std::uint8_t baseFocusIndex; + std::uint8_t oppositeDirection; + std::uint8_t characterTextCount; + std::uint8_t baseTextCount; + std::uint8_t markTextCount; + std::uint32_t marksTextCount; + std::uint32_t marksTextNumber; + std::uint32_t reserved[3]; + std::int32_t textLetterOffset; + } Components; +}; + +struct FontWritingExtent { + float top; + float bottom; + float left; + float right; +}; + +struct FontWritingMetrics { + float advanceX; + float advanceY; + FontWritingExtent Extent; +}; + +struct FontHorizontalLayout { + float baselineOffset; + float lineAdvance; + float decorationExtent; +}; + +struct FontVerticalLayout { + float baselineOffsetX; + float columnAdvance; + float decorationSpan; +}; + +static_assert(sizeof(FontHandleOpaque) == 0x100); +static_assert(sizeof(FontMemory) == 0x40); +static_assert(sizeof(FontMemoryInterface) == 0x30); +static_assert(sizeof(FontOpenDetail) == 0x20); +static_assert(sizeof(FontGlyphMetrics) == 0x20); +static_assert(sizeof(FontKerning) == 0x10); +static_assert(sizeof(FontGlyphImageMetrics) == 0x18); +static_assert(sizeof(FontGenerateGlyphDetail) == 0x20); +static_assert(sizeof(FontResultStage) == 0x18); +static_assert(sizeof(FontSurfaceImage) == 0x10); +static_assert(sizeof(FontRenderOutput) == 0x40); +static_assert(sizeof(FontTextCharacter) == 0xC8); +static_assert(offsetof(FontTextCharacter, flags) == 0x38); +static_assert(sizeof(FontRenderSurface) == 0x80); +static_assert(sizeof(FontStyleFrame) == 0x60); +static_assert(offsetof(FontStyleFrame, scalePixelW) == 0x14); +static_assert(offsetof(FontStyleFrame, cache_lock_word) == 0x34); +static_assert(offsetof(FontStyleFrame, reserved_0x58) == 0x58); +static_assert(sizeof(FontTextSource) == 0x60); +static_assert(offsetof(FontTextSource, textParser) == 0x20); +static_assert(offsetof(FontTextSource, defaultFont) == 0x30); +static_assert(offsetof(FontTextSource, systemUse) == 0x38); +static_assert(sizeof(FontCreateStringDetail) == 0x20); +static_assert(sizeof(FontWriting) == 0x100); +static_assert(sizeof(FontWritingProfile) == sizeof(std::uint32_t)); +static_assert(sizeof(FontWritingStep) == 0x48); +static_assert(sizeof(FontWritingLetterStep) == 0x40); +static_assert(sizeof(FontWritingMetrics) == 0x18); + +#endif diff --git a/core/libs/prx/libSceFont/src/Layout.cpp b/core/libs/prx/libSceFont/src/Layout.cpp new file mode 100644 index 000000000..7b76c46d0 --- /dev/null +++ b/core/libs/prx/libSceFont/src/Layout.cpp @@ -0,0 +1,522 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include + +#include +#include FT_FREETYPE_H +#include FT_OUTLINE_H + +#include "prx/libSceFont/include/FontFreeType.hpp" +#include "prx/libSceFont/include/FontInternal.hpp" + +namespace { + +using namespace Font; + +struct FloatPair { + float lo = 0.0f; + float hi = 0.0f; +}; + +bool OrderedLess(float a, float b) { + return !std::isnan(a) && !std::isnan(b) && a < b; +} + +float MaxSs(float a, float b) { + if (std::isnan(a) || std::isnan(b)) return b; + if (a > b) return a; + if (a < b) return b; + if (a == 0.0f && b == 0.0f && std::signbit(a) != std::signbit(b)) return std::signbit(a) ? b : a; + return a; +} + +float FlipSign(float value) { + return std::bit_cast(std::bit_cast(value) ^ 0x80000000u); +} + +void UpdateShiftCache(FontObj* obj) { + obj->shift_cache_x = obj->shift_units_x; + obj->shift_cache_y = obj->shift_units_y; + const auto seedLow = static_cast(static_cast(obj->layout_seed_pair & 0xFFFFFFFFu)); + const auto seedHigh = static_cast(static_cast(obj->layout_seed_pair >> 32)); + obj->layout_seed_vec[0] = static_cast(static_cast(seedLow)); + obj->layout_seed_vec[1] = static_cast(static_cast(seedHigh)); + obj->layout_scale_vec[0] = 0x10000; + obj->layout_scale_vec[1] = 0x10000; +} + +int SetDriverCharSize(const SysDriver* driver, FontObj* obj, const StyleStateBlock* style, float& scaleX, float& scaleY) { + if (style->scale_unit == 0) return driver->set_char_default_dpi(obj, scaleX, scaleY, &scaleX, &scaleY); + return driver->set_char_with_dpi(obj, style->dpi_x, style->dpi_y, scaleX, scaleY, &scaleX, &scaleY); +} + +void ClearHorizontalLayout(std::uint8_t* outWords) { + if (!outWords) return; + std::memset(outWords, 0, 0x10); + std::memset(outWords + 0x18, 0, 0x0C); +} + +void ClearVerticalLayout(std::uint8_t* outWords) { + if (!outWords) return; + std::memset(outWords, 0, 0x10); + std::memset(outWords + 0x14, 0, 0x0C); +} + +const StyleStateBlock* ResolveSurfaceStyleState(const StyleStateBlock* cachedState, const FontRenderSurface* surface, StyleStateBlock& temporary) { + const auto* systemUse = reinterpret_cast(surface->reserved_q); + if ((surface->styleFlag & 0x1) == 0 || !ValidStyleFrame(systemUse->styleframe)) return cachedState; + const FontStyleFrame* frame = systemUse->styleframe; + temporary = *cachedState; + if ((frame->flags1 & STYLE_FRAME_FLAG_SCALE) != 0) { + temporary.scale_unit = frame->scaleUnit; + temporary.scale_w = frame->scalePixelW; + temporary.scale_h = frame->scalePixelH; + temporary.dpi_x = frame->hDpi ? frame->hDpi : 0x48; + temporary.dpi_y = frame->vDpi ? frame->vDpi : 0x48; + } else { + const std::uint32_t wantX = frame->hDpi == 0 ? temporary.dpi_x : frame->hDpi; + const std::uint32_t wantY = frame->vDpi == 0 ? temporary.dpi_y : frame->vDpi; + temporary.dpi_x = wantX != 0 ? 0x48 : 0; + temporary.dpi_y = wantY != 0 ? 0x48 : 0; + } + if ((frame->flags1 & STYLE_FRAME_FLAG_SLANT) != 0) temporary.slant_ratio = frame->slantRatio; + if ((frame->flags1 & STYLE_FRAME_FLAG_WEIGHT) != 0) { + temporary.effect_weight_x = frame->effectWeightX; + temporary.effect_weight_y = frame->effectWeightY; + } + return &temporary; +} + +void FillImageMetrics(FontRenderOutput* result, const FontGlyphMetrics* metrics, float x, float y) { + const float leftF = x + metrics->Horizontal.bearingX; + const float topF = y + metrics->Horizontal.bearingY; + const float rightF = leftF + metrics->width; + const float bottomF = topF - metrics->height; + const int left = FloorToInt(leftF); + const int top = CeilToInt(topF); + const int right = CeilToInt(rightF); + const int bottom = FloorToInt(bottomF); + const float advanceF = x + metrics->Horizontal.advance; + const int advance = CeilToInt(advanceF); + result->ImageMetrics.bearingX = static_cast(left) - x; + result->ImageMetrics.bearingY = static_cast(top) - y; + result->ImageMetrics.advance = static_cast(advance) - x; + int stride = advance; + if (advanceF != static_cast(advance)) { + const int candidate = static_cast((static_cast(right) - rightF) + advanceF); + stride = right + 1; + if (advanceF <= rightF) stride = candidate; + if (stride < candidate) stride = candidate; + } + result->ImageMetrics.stride = static_cast(stride) - x; + result->ImageMetrics.width = static_cast(std::max(0, right - left)); + result->ImageMetrics.height = static_cast(std::max(0, top - bottom)); +} + +int RenderGlyphIndexToSurface(FontObj& obj, std::uint32_t glyphIndex, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result) { + ClearRenderOutputs(metrics, result); + if (!surface->buffer || surface->width <= 0 || surface->height <= 0 || surface->widthByte <= 0 || surface->pixelSizeByte <= 0) return SCE_FONT_ERROR_NO_SUPPORT_SURFACE; + const int bytesPerPixel = surface->pixelSizeByte; + if (bytesPerPixel != 1 && bytesPerPixel != 4) return SCE_FONT_ERROR_NO_SUPPORT_SURFACE; + const auto face = static_cast(obj.ft_face); + if (!face || !face->size) return SCE_FONT_ERROR_NO_SUPPORT_GLYPH; + FT_Vector delta{}; + delta.x = static_cast(static_cast((x - std::floor(x)) * 64.0f)); + delta.y = static_cast(-static_cast((y - std::floor(y)) * 64.0f)); + if (obj.shift_units_x != 0 || obj.shift_units_y != 0) { + delta.x += static_cast(RoundMulFixed(obj.shift_units_x, static_cast(face->size->metrics.x_scale))); + delta.y += static_cast(RoundMulFixed(obj.shift_units_y, static_cast(face->size->metrics.y_scale))); + } + FT_Set_Transform(face, nullptr, &delta); + const FT_Error error = FT_Load_Glyph(face, glyphIndex, FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP | FT_LOAD_VERTICAL_LAYOUT | FT_LOAD_RENDER); + FT_Set_Transform(face, nullptr, nullptr); + if (error != 0 || !face->glyph) return SCE_FONT_ERROR_NO_SUPPORT_GLYPH; + const FT_GlyphSlot slot = face->glyph; + const int glyphW = static_cast(slot->bitmap.width); + const int glyphH = static_cast(slot->bitmap.rows); + metrics->width = static_cast(slot->metrics.width) / 64.0f; + metrics->height = static_cast(slot->metrics.height) / 64.0f; + metrics->Horizontal.bearingX = static_cast(slot->metrics.horiBearingX) / 64.0f; + metrics->Horizontal.bearingY = static_cast(slot->metrics.horiBearingY) / 64.0f; + metrics->Horizontal.advance = static_cast(slot->metrics.horiAdvance) / 64.0f; + std::vector bitmap(static_cast(glyphW) * static_cast(glyphH)); + if (glyphW > 0 && glyphH > 0) { + const int pitch = slot->bitmap.pitch; + const auto* source = slot->bitmap.buffer; + for (int row = 0; row < glyphH; ++row) { + const std::uint8_t* sourceRow = source + static_cast(row) * pitch; + std::uint8_t* targetRow = bitmap.data() + static_cast(row) * static_cast(glyphW); + if (slot->bitmap.pixel_mode == FT_PIXEL_MODE_MONO) { + for (int column = 0; column < glyphW; ++column) targetRow[column] = (sourceRow[column >> 3] & (0x80u >> (column & 7))) ? 0xFF : 0x00; + } else { + std::memcpy(targetRow, sourceRow, static_cast(glyphW)); + } + } + } + const int destX = static_cast(std::floor(x)) + slot->bitmap_left; + const int destY = static_cast(std::floor(y)) - slot->bitmap_top; + const int clipX0 = std::clamp(static_cast(surface->sc_x0), 0, surface->width); + const int clipY0 = std::clamp(static_cast(surface->sc_y0), 0, surface->height); + const int clipX1 = std::clamp(static_cast(surface->sc_x1), 0, surface->width); + const int clipY1 = std::clamp(static_cast(surface->sc_y1), 0, surface->height); + int updateX = destX; + int updateY = destY; + int updateW = 0; + int updateH = 0; + if (glyphW > 0 && glyphH > 0 && clipX1 > clipX0 && clipY1 > clipY0) { + auto* targetBase = static_cast(surface->buffer); + const int startRow = std::max(destY, clipY0); + const int endRow = std::min(destY + glyphH, clipY1); + const int startColumn = std::max(destX, clipX0); + const int endColumn = std::min(destX + glyphW, clipX1); + updateX = startColumn; + updateY = startRow; + updateW = std::max(0, endColumn - startColumn); + updateH = std::max(0, endRow - startRow); + for (int row = startRow; row < endRow; ++row) { + const std::uint8_t* sourceRow = bitmap.data() + static_cast(row - destY) * static_cast(glyphW); + std::uint8_t* targetRow = targetBase + static_cast(row) * static_cast(surface->widthByte); + for (int column = startColumn; column < endColumn; ++column) { + const std::uint8_t coverage = sourceRow[column - destX]; + std::memset(targetRow + static_cast(column) * static_cast(bytesPerPixel), coverage, static_cast(bytesPerPixel)); + } + } + } + result->stage = nullptr; + result->SurfaceImage.widthByte = static_cast(surface->widthByte); + result->SurfaceImage.pixelSizeByte = static_cast(surface->pixelSizeByte); + result->SurfaceImage.pixelFormat = 0; + result->SurfaceImage.pad16 = 0; + result->UpdateRect.x = static_cast(std::max(updateX, 0)); + result->UpdateRect.y = static_cast(std::max(updateY, 0)); + result->UpdateRect.w = static_cast(std::max(updateW, 0)); + result->UpdateRect.h = static_cast(std::max(updateH, 0)); + result->SurfaceImage.address = static_cast(surface->buffer) + static_cast(result->UpdateRect.y) * static_cast(surface->widthByte) + static_cast(result->UpdateRect.x) * static_cast(bytesPerPixel); + FillImageMetrics(result, metrics, x, y); + return SCE_FONT_OK; +} + +} + +int Font::ComputeHorizontalLayout(FontHandle handle, const StyleStateBlock* style, std::uint8_t* outWords) { + ClearHorizontalLayout(outWords); + const auto fail = [&] { + ClearHorizontalLayout(outWords); + return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + }; + auto* font = GetNativeFont(handle); + if (!font) return fail(); + auto* library = static_cast(font->library); + if (!library || !library->external_fonts_ctx) return fail(); + const SysDriver* driver = library->sys_driver; + if (!driver || !driver->set_char_with_dpi || !driver->set_char_default_dpi || !driver->compute_layout || !style) return fail(); + const std::uint32_t modeLow = font->flags & 0x0Fu; + const std::uint32_t fontId = font->open_info.ctx_entry_index; + float baselineMax = 0.0f; + float deltaMax = 0.0f; + float effectForBaseline = 0.0f; + float effectForDelta = 0.0f; + FloatPair accumulatedAdjust{}; + FloatPair accumulatedBounds{}; + if (static_cast(fontId) >= 0) { + FontObj* head = nullptr; + std::uint32_t lockWord = 0; + auto* entry = AcquireFontCtxEntry(library->external_fonts_ctx, fontId, modeLow, &head, &lockWord); + if (!entry) return fail(); + FontObj* match = FindSubFont(head, font->open_info.sub_font_index); + int rc = SCE_FONT_ERROR_FATAL; + std::uint8_t layout[HORIZONTAL_LAYOUT_SIZE] = {}; + if ((lockWord & OPEN_BIT) != 0 && (lockWord & COUNT_MASK) != 0 && match) { + float scaleX = style->scale_w; + float scaleY = style->scale_h; + match->font_handle = handle; + match->shift_units_x = 0; + match->shift_units_y = 0; + rc = SetDriverCharSize(driver, match, style, scaleX, scaleY); + if (rc == SCE_FONT_OK) { + UpdateShiftCache(match); + rc = driver->compute_layout(match, style, layout); + } + } + ReleaseFontCtxEntryLock(entry, modeLow, lockWord); + if (rc != SCE_FONT_OK) return fail(); + const float lineAdvance = LoadFloat(layout, HORIZONTAL_LINE_ADVANCE); + const float baseline = LoadFloat(layout, HORIZONTAL_BASELINE); + const float effectHeight = LoadFloat(layout, HORIZONTAL_EFFECT_HEIGHT); + const FloatPair bounds{LoadFloat(layout, HORIZONTAL_X_BOUND_LO), LoadFloat(layout, HORIZONTAL_X_BOUND_HI)}; + const FloatPair adjust{LoadFloat(layout, HORIZONTAL_HALF_EFFECT_WIDTH), LoadFloat(layout, HORIZONTAL_LEFT_ADJUST)}; + const FloatPair previousAdjust = accumulatedAdjust; + const FloatPair previousBounds = accumulatedBounds; + const bool baselineUpdate = OrderedLess(baselineMax, baseline); + baselineMax = MaxSs(baseline, baselineMax); + if (baselineUpdate) effectForBaseline = effectHeight; + const float delta = lineAdvance - baseline; + const bool deltaUpdate = OrderedLess(deltaMax, delta); + deltaMax = MaxSs(delta, deltaMax); + if (deltaUpdate) effectForDelta = effectHeight; + const FloatPair difference{bounds.lo - adjust.hi, bounds.hi - adjust.lo}; + const FloatPair sum{previousAdjust.lo + previousBounds.lo, previousAdjust.hi + previousBounds.hi}; + if (OrderedLess(difference.lo, sum.lo)) { + accumulatedBounds.lo = bounds.lo; + accumulatedAdjust.lo = FlipSign(adjust.hi); + } + if (OrderedLess(difference.hi, sum.hi)) { + accumulatedBounds.hi = bounds.hi; + accumulatedAdjust.hi = adjust.lo; + } + } + if (!outWords) return fail(); + StoreFloat(outWords, HORIZONTAL_BASELINE, baselineMax); + StoreFloat(outWords, HORIZONTAL_LINE_ADVANCE, baselineMax + deltaMax); + StoreFloat(outWords, HORIZONTAL_X_BOUND_LO, accumulatedBounds.lo); + StoreFloat(outWords, HORIZONTAL_X_BOUND_HI, accumulatedBounds.hi); + StoreFloat(outWords, HORIZONTAL_EFFECT_HEIGHT, MaxSs(effectForBaseline, effectForDelta)); + StoreFloat(outWords, HORIZONTAL_LEFT_ADJUST, accumulatedAdjust.lo); + StoreFloat(outWords, HORIZONTAL_HALF_EFFECT_WIDTH, accumulatedAdjust.hi); + return SCE_FONT_OK; +} + +int Font::ComputeVerticalLayout(FontHandle handle, const StyleStateBlock* style, std::uint8_t* outWords) { + const auto fail = [&](int rc) { + ClearVerticalLayout(outWords); + return rc; + }; + auto* font = GetNativeFont(handle); + auto* library = font ? static_cast(font->library) : nullptr; + if (!library || !library->external_fonts_ctx || !library->sys_driver) return fail(SCE_FONT_ERROR_INVALID_FONT_HANDLE); + const SysDriver* driver = library->sys_driver; + const std::uint32_t modeLow = font->flags & 0x0Fu; + const std::uint32_t fontId = font->open_info.ctx_entry_index; + float accNegOffsetMax = 0.0f; + float accSumMax = 0.0f; + float spanForNegOffset = 0.0f; + float spanForSum = 0.0f; + float accMetrics08Max = 0.0f; + float accMetrics0CMax = 0.0f; + float accDiffMin = 0.0f; + float accTempMin = 0.0f; + if (static_cast(fontId) >= 0) { + FontObj* head = nullptr; + std::uint32_t lockWord = 0; + auto* entry = AcquireFontCtxEntry(library->external_fonts_ctx, fontId, modeLow, &head, &lockWord); + FontObj* obj = FindSubFont(head, font->open_info.sub_font_index); + if (!entry || !obj) { + if (entry) ReleaseFontCtxEntryLock(entry, modeLow, lockWord); + return fail(SCE_FONT_ERROR_FATAL); + } + int rc = SCE_FONT_ERROR_FATAL; + if ((lockWord & OPEN_BIT) != 0 && (lockWord & COUNT_MASK) != 0) { + float scaleX = style ? style->scale_w : 0.0f; + float scaleY = style ? style->scale_h : 0.0f; + obj->font_handle = handle; + obj->shift_units_x = 0; + obj->shift_units_y = 0; + int callRc = SCE_FONT_ERROR_FATAL; + if (style && style->scale_unit == 0) { + if (driver->set_char_default_dpi) callRc = driver->set_char_default_dpi(obj, scaleX, scaleY, &scaleX, &scaleY); + } else if (driver->set_char_with_dpi) { + callRc = driver->set_char_with_dpi(obj, style ? style->dpi_x : 0u, style ? style->dpi_y : 0u, scaleX, scaleY, &scaleX, &scaleY); + } + std::uint8_t layout[VERTICAL_LAYOUT_SIZE] = {}; + if (callRc == SCE_FONT_OK) { + UpdateShiftCache(obj); + callRc = driver->compute_layout_alt ? driver->compute_layout_alt(obj, style, layout) : SCE_FONT_ERROR_FATAL; + } + if (callRc == SCE_FONT_OK) { + const float metrics00 = LoadFloat(layout, VERTICAL_COLUMN_ADVANCE); + const float offsetCandidate = LoadFloat(layout, VERTICAL_BASELINE_OFFSET_X); + const float metrics08 = LoadFloat(layout, VERTICAL_METRICS_0X08); + const float metrics0C = LoadFloat(layout, VERTICAL_METRICS_0X0C); + const float spanCandidate = LoadFloat(layout, VERTICAL_DECORATION_SPAN); + const float extra08 = LoadFloat(layout, VERTICAL_DECORATION_0X08); + const float extra0C = LoadFloat(layout, VERTICAL_DECORATION_0X0C); + const float negOffset = -offsetCandidate; + const float sum = offsetCandidate + metrics00; + if (accNegOffsetMax < negOffset) spanForNegOffset = spanCandidate; + accNegOffsetMax = std::max(accNegOffsetMax, negOffset); + if (accSumMax < sum) spanForSum = spanCandidate; + accSumMax = std::max(accSumMax, sum); + accDiffMin = std::min(extra08 - offsetCandidate, accDiffMin); + accMetrics08Max = std::max(metrics08, accMetrics08Max); + accTempMin = std::min(sum + extra0C, accDiffMin); + accMetrics0CMax = std::max(metrics0C, accMetrics0CMax); + rc = SCE_FONT_OK; + } else { + rc = callRc; + } + } + ReleaseFontCtxEntryLock(entry, modeLow, lockWord); + if (rc != SCE_FONT_OK) return fail(rc); + } + if (outWords) { + StoreFloat(outWords, VERTICAL_COLUMN_ADVANCE, accNegOffsetMax + accSumMax); + StoreFloat(outWords, VERTICAL_BASELINE_OFFSET_X, -accNegOffsetMax); + StoreFloat(outWords, VERTICAL_METRICS_0X08, accMetrics08Max); + StoreFloat(outWords, VERTICAL_METRICS_0X0C, accMetrics0CMax); + StoreFloat(outWords, VERTICAL_DECORATION_SPAN, std::max(spanForSum, spanForNegOffset)); + StoreFloat(outWords, VERTICAL_DECORATION_0X08, accTempMin - accNegOffsetMax); + StoreFloat(outWords, VERTICAL_DECORATION_0X0C, -accSumMax); + } + return SCE_FONT_OK; +} + +int Font::GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMetrics* metrics, bool useCachedStyle) { + const auto clearMetrics = [&] { + if (metrics) *metrics = {}; + }; + auto* font = GetNativeFont(handle); + if (!font || font->magic != HANDLE_MAGIC) { + clearMetrics(); + return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + } + if (code == 0) { + clearMetrics(); + return SCE_FONT_ERROR_NO_SUPPORT_CODE; + } + if (!metrics) return SCE_FONT_ERROR_INVALID_PARAMETER; + std::uint32_t previousFontLock = 0; + if (!AcquireFontLock(font, previousFontLock)) { + clearMetrics(); + return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + } + std::uint32_t previousCachedLock = 0; + if (useCachedStyle && !AcquireCachedStyleLock(font, previousCachedLock)) { + ReleaseFontLock(font, previousFontLock); + clearMetrics(); + return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + } + const auto finish = [&](int rc) { + if (useCachedStyle) ReleaseCachedStyleLock(font, previousCachedLock); + ReleaseFontLock(font, previousFontLock); + if (rc != SCE_FONT_OK) clearMetrics(); + return rc; + }; + FontState* state = TryGetState(handle); + if (!state) return finish(SCE_FONT_ERROR_INVALID_FONT_HANDLE); + if (useCachedStyle && !font->renderer) return finish(SCE_FONT_ERROR_NOT_BOUND_RENDERER); + const StyleStateBlock* style = useCachedStyle ? &font->cached_style.state : &font->style; + float scaleW = 0.0f; + float scaleH = 0.0f; + const int scaleRc = StyleStateGetScalePixel(style, &scaleW, &scaleH); + if (scaleRc != SCE_FONT_OK) return finish(scaleRc); + const FT_Face face = state->face; + if (!face) return finish(SCE_FONT_ERROR_NO_SUPPORT_FUNCTION); + const FT_UInt glyphIndex = ResolveGlyphIndexWithFallback(face, code); + if (glyphIndex == 0) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + const auto charW = static_cast(static_cast(scaleW * 64.0f)); + const auto charH = static_cast(static_cast(scaleH * 64.0f)); + FT_F26Dot6 usedW = charW; + FT_F26Dot6 usedH = charH; + if (SetCharSizeCompat(face, charW, charH, 72, 72, &usedW, &usedH) != 0) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + FT_Set_Transform(face, nullptr, nullptr); + if (FT_Load_Glyph(face, glyphIndex, FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP | FT_LOAD_VERTICAL_LAYOUT) != 0) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + const auto ratio = [](FT_F26Dot6 requested, FT_F26Dot6 applied) { + if (requested == 0 || applied == 0) return 1.0f; + const double value = static_cast(std::llabs(static_cast(requested))) / static_cast(std::llabs(static_cast(applied))); + return std::isfinite(value) && value > 0.0 ? static_cast(value) : 1.0f; + }; + const float ratioX = ratio(charW, usedW); + const float ratioY = ratio(charH, usedH); + const FT_GlyphSlot slot = face->glyph; + metrics->width = static_cast(slot->metrics.width) / 64.0f * ratioX; + metrics->height = static_cast(slot->metrics.height) / 64.0f * ratioY; + metrics->Horizontal.bearingX = static_cast(slot->metrics.horiBearingX) / 64.0f * ratioX; + metrics->Horizontal.bearingY = static_cast(slot->metrics.horiBearingY) / 64.0f * ratioY; + metrics->Horizontal.advance = static_cast(slot->metrics.horiAdvance) / 64.0f * ratioX; + metrics->Vertical.bearingX = 0.0f; + metrics->Vertical.bearingY = 0.0f; + metrics->Vertical.advance = 0.0f; + return finish(SCE_FONT_OK); +} + +int Font::RenderCharGlyphImageCore(FontHandle handle, std::uint32_t code, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result) { + const auto fail = [&](int rc) { + ClearRenderOutputs(metrics, result); + return rc; + }; + if (!handle) return fail(SCE_FONT_ERROR_INVALID_FONT_HANDLE); + if (code == 0) return fail(SCE_FONT_ERROR_NO_SUPPORT_CODE); + if (!surface || !metrics || !result) return fail(SCE_FONT_ERROR_INVALID_PARAMETER); + auto* font = GetNativeFont(handle); + if (font->magic != HANDLE_MAGIC) return fail(SCE_FONT_ERROR_INVALID_FONT_HANDLE); + if (!font->renderer) return fail(SCE_FONT_ERROR_NOT_BOUND_RENDERER); + auto* library = static_cast(font->library); + if (!library || library->magic != LIBRARY_MAGIC) return fail(SCE_FONT_ERROR_INVALID_FONT_HANDLE); + const SysDriver* driver = library->sys_driver; + if (!driver || !driver->glyph_index || !driver->set_char_with_dpi || !driver->set_char_default_dpi) return fail(SCE_FONT_ERROR_FATAL); + const std::uint32_t modeLow = font->flags & 0x0Fu; + StyleStateBlock surfaceStyle{}; + const StyleStateBlock* style = ResolveSurfaceStyleState(&font->cached_style.state, surface, surfaceStyle); + FontObj* obj = nullptr; + std::uint32_t lockWord = 0; + auto* entry = AcquireFontCtxEntry(library->external_fonts_ctx, font->open_info.ctx_entry_index, modeLow, &obj, &lockWord); + obj = FindSubFont(obj, font->open_info.sub_font_index); + if (!entry || !obj) { + if (entry) ReleaseFontCtxEntryLock(entry, modeLow, lockWord); + return fail(SCE_FONT_ERROR_FATAL); + } + const auto finish = [&](int rc) { + ReleaseFontCtxEntryLock(entry, modeLow, lockWord); + if (rc != SCE_FONT_OK) ClearRenderOutputs(metrics, result); + return rc; + }; + if ((lockWord & OPEN_BIT) == 0 || (lockWord & COUNT_MASK) == 0) return finish(SCE_FONT_ERROR_FATAL); + obj->reserved_0x04 = static_cast(font->flags >> 15); + obj->font_handle = handle; + obj->shift_units_x = 0; + obj->shift_units_y = 0; + obj->layout_seed_pair = 0; + StyleStateGetScalePixel(style, &obj->scale_x_0x50, &obj->scale_y_0x54); + float outScaleX = 0.0f; + float outScaleY = 0.0f; + int rc; + if (style->scale_unit == 0) { + rc = driver->set_char_default_dpi(obj, style->scale_w, style->scale_h, &outScaleX, &outScaleY); + } else { + rc = driver->set_char_with_dpi(obj, style->dpi_x ? style->dpi_x : 0x48, style->dpi_y ? style->dpi_y : 0x48, style->scale_w, style->scale_h, &outScaleX, &outScaleY); + } + if (rc != SCE_FONT_OK) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + std::uint32_t glyphIndex = 0; + rc = driver->glyph_index(obj, code, &glyphIndex); + if (rc != SCE_FONT_OK || glyphIndex == 0) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + return finish(RenderGlyphIndexToSurface(*obj, glyphIndex, surface, x, y, metrics, result)); +} + +bool Font::BuildTrueOutline(GeneratedGlyph& glyph) { + FontState* state = TryGetState(glyph.owner); + if (!state || !state->face) return false; + const auto* font = GetNativeFont(glyph.owner); + if (font->magic != HANDLE_MAGIC) return false; + const FT_Face face = state->face; + const FT_UInt glyphIndex = FT_Get_Char_Index(face, glyph.codepoint); + if (glyphIndex == 0) return false; + const auto charW = static_cast(static_cast(glyph.glyph.scale_x * 64.0f)); + const auto charH = static_cast(static_cast(glyph.glyph.base_scale * 64.0f)); + if (FT_Set_Char_Size(face, charW, charH, 72, 72) != 0) return false; + FT_Set_Transform(face, nullptr, nullptr); + if (FT_Load_Glyph(face, glyphIndex, FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP) != 0) return false; + const FT_GlyphSlot slot = face->glyph; + if (!slot || slot->format != FT_GLYPH_FORMAT_OUTLINE) return false; + const FT_Outline& outline = slot->outline; + if (outline.n_points <= 0 || outline.n_contours <= 0 || !outline.points || !outline.tags || !outline.contours) return false; + glyph.outlinePoints.clear(); + glyph.outlineTags.clear(); + glyph.outlineContours.clear(); + for (int i = 0; i < outline.n_points; ++i) { + glyph.outlinePoints.push_back({static_cast(outline.points[i].x) / 64.0f, static_cast(outline.points[i].y) / 64.0f}); + glyph.outlineTags.push_back(FT_CURVE_TAG(outline.tags[i]) == FT_CURVE_TAG_ON ? 1 : 0); + } + for (int i = 0; i < outline.n_contours; ++i) glyph.outlineContours.push_back(static_cast(outline.contours[i])); + glyph.outline.points_ptr = glyph.outlinePoints.data(); + glyph.outline.tags_ptr = glyph.outlineTags.data(); + glyph.outline.contour_end_idx = glyph.outlineContours.data(); + glyph.outline.points_cnt = static_cast(glyph.outlinePoints.size()); + glyph.outline.contours_cnt = static_cast(glyph.outlineContours.size()); + glyph.outlineInitialized = true; + return true; +} diff --git a/core/libs/prx/libSceFont/src/Library.cpp b/core/libs/prx/libSceFont/src/Library.cpp new file mode 100644 index 000000000..46b421dcb --- /dev/null +++ b/core/libs/prx/libSceFont/src/Library.cpp @@ -0,0 +1,378 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include + +#include "prx/libSceFont/include/FontInternal.hpp" + +namespace { + +using namespace Font; + +std::uint32_t EffectiveEdition(std::uint64_t edition) { + return static_cast(edition >> 32); +} + +std::uint32_t* AcquireDeviceCache(FontLibNative* library) { + auto* const busy = reinterpret_cast(std::numeric_limits::max()); + std::atomic_ref slot(library->device_cache_buf); + for (;;) { + std::uint32_t* current = slot.load(std::memory_order_acquire); + if (current != busy && slot.compare_exchange_weak(current, busy, std::memory_order_acq_rel)) return current; + Backoff(); + } +} + +void ReleaseDeviceCache(FontLibNative* library, std::uint32_t* cache) { + std::atomic_ref(library->device_cache_buf).store(cache, std::memory_order_release); +} + +void* AcquireRendererSelection(RendererNative* renderer) { + auto* const busy = reinterpret_cast(std::numeric_limits::max()); + std::atomic_ref slot(renderer->selection); + for (;;) { + void* current = slot.load(std::memory_order_acquire); + if (current != busy && slot.compare_exchange_weak(current, busy, std::memory_order_acq_rel)) return current; + Backoff(); + } +} + +void* CreateFontContext(FontLibNative* library, std::uint32_t entries, std::uint32_t size) { + void* ctx = library->iface->alloc(library->alloc_ctx, size); + if (!ctx) return nullptr; + std::memset(ctx, 0, size); + auto* header = static_cast(ctx); + header->lock_word = 0; + header->max_entries = entries; + header->base = entries != 0 ? static_cast(ctx) + sizeof(FontCtxHeader) : nullptr; + return ctx; +} + +int SupportFonts(FontLibrary library, void* FontLibNative::*slot, std::uint32_t entries, std::uint32_t size, std::uint32_t formats) { + auto* lib = static_cast(library); + if (!lib || lib->magic != LIBRARY_MAGIC) return SCE_FONT_ERROR_INVALID_LIBRARY; + std::uint32_t previous = 0; + if (!AcquireLibraryLock(lib, previous)) return SCE_FONT_ERROR_INVALID_LIBRARY; + const auto finish = [&](int rc) { + ReleaseLibraryLock(lib, previous); + return rc; + }; + if (lib->*slot) return finish(SCE_FONT_ERROR_ALREADY_SPECIFIED); + void* ctx = CreateFontContext(lib, entries, size); + if (!ctx) return finish(SCE_FONT_ERROR_ALLOCATION_FAILED); + if (!lib->sys_driver || !lib->sys_driver->support_formats) { + lib->iface->dealloc(lib->alloc_ctx, ctx); + return finish(SCE_FONT_ERROR_INVALID_LIBRARY); + } + const int rc = lib->sys_driver->support_formats(lib, formats); + if (rc != SCE_FONT_OK) { + lib->iface->dealloc(lib->alloc_ctx, ctx); + return finish(rc); + } + lib->*slot = ctx; + return finish(SCE_FONT_OK); +} + +} + +#pragma GCC visibility push(default) + +extern "C" { + +int APS5_VABI sceFontMemoryInit(FontMemory* memory, void* regionAddress, std::uint32_t regionSize, const FontMemoryInterface* iface, void* mspaceObject, FontMemoryDestroyFunction destroyCallback, void* destroyObject) { + if (!memory) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (!iface) { + memory->mem_kind = 0; + if (!regionAddress || regionSize == 0) return SCE_FONT_ERROR_INVALID_PARAMETER; + } + memory->mem_kind = MEMORY_MAGIC; + memory->attr_bits = 0; + memory->region_size = regionSize; + memory->region_base = regionAddress; + memory->mspace_handle = mspaceObject; + memory->iface = iface; + memory->on_destroy = destroyCallback; + memory->destroy_ctx = destroyObject; + memory->some_ctx1 = nullptr; + memory->some_ctx2 = mspaceObject; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontMemoryTerm(FontMemory* memory) { + if (!memory) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (memory->mem_kind != MEMORY_MAGIC) return SCE_FONT_ERROR_INVALID_MEMORY; + if (static_cast(memory->attr_bits & 0xFF) < 0) { + if (memory->iface && memory->iface->mspace_destroy) memory->iface->mspace_destroy(memory->some_ctx2, memory->mspace_handle); + std::memset(memory, 0, sizeof(*memory)); + return SCE_FONT_OK; + } + if (memory->on_destroy) { + memory->mem_kind = 0; + memory->on_destroy(memory, memory->mspace_handle, memory->destroy_ctx); + return SCE_FONT_OK; + } + std::memset(memory, 0, sizeof(*memory)); + return SCE_FONT_OK; +} + +int APS5_VABI sceFontCreateLibraryWithEdition(const FontMemory* memory, const void* createParams, std::uint64_t edition, FontLibrary* pLibrary) { + if (pLibrary) *pLibrary = nullptr; + if (!memory) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (memory->mem_kind != MEMORY_MAGIC || !memory->iface || !memory->iface->alloc || !memory->iface->dealloc) return SCE_FONT_ERROR_INVALID_MEMORY; + if (!createParams || !pLibrary) return SCE_FONT_ERROR_INVALID_PARAMETER; + const auto allocFn = memory->iface->alloc; + const auto freeFn = memory->iface->dealloc; + auto* lib = static_cast(allocFn(memory->mspace_handle, sizeof(FontLibNative))); + if (!lib) return SCE_FONT_ERROR_ALLOCATION_FAILED; + void* workspace = allocFn(memory->mspace_handle, 0x4000); + if (!workspace) { + freeFn(memory->mspace_handle, lib); + return SCE_FONT_ERROR_ALLOCATION_FAILED; + } + std::memset(lib, 0, sizeof(FontLibNative)); + lib->mem_kind = MEMORY_MAGIC; + lib->region_size = memory->region_size; + lib->region_base = memory->region_base; + lib->alloc_ctx = memory->mspace_handle; + lib->iface = memory->iface; + lib->alloc_fn = memory->iface->alloc; + lib->dealloc_fn = memory->iface->dealloc; + lib->realloc_fn = memory->iface->realloc_fn; + lib->calloc_fn = memory->iface->calloc_fn; + lib->sys_driver = static_cast(createParams); + lib->workspace_size = 0x4000; + lib->workspace = workspace; + lib->list_head_ptr = &lib->list_head; + lib->list_head = nullptr; + const auto initFn = lib->sys_driver->init; + const int rc = initFn ? initFn(memory, lib) : SCE_FONT_ERROR_INVALID_PARAMETER; + if (rc != SCE_FONT_OK) { + freeFn(memory->mspace_handle, workspace); + freeFn(memory->mspace_handle, lib); + return rc; + } + const std::uint32_t version = EffectiveEdition(edition); + if (version > 0x92FFFFu) { + if (version < 0x1500000u) { + lib->feature_word |= 1u | 2u | 4u; + } else if (version < 0x2000000u) { + lib->feature_word |= 2u | 4u; + } else if (version <= 0x2000070u) { + lib->feature_word |= 4u; + } + } + lib->magic = LIBRARY_MAGIC; + *pLibrary = lib; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontCreateLibrary(const FontMemory* memory, const void* createParams, FontLibrary* pLibrary) { + return sceFontCreateLibraryWithEdition(memory, createParams, 0, pLibrary); +} + +int APS5_VABI sceFontDestroyLibrary(FontLibrary* pLibrary) { + if (!pLibrary) return SCE_FONT_ERROR_INVALID_PARAMETER; + auto* lib = static_cast(*pLibrary); + if (!lib || lib->magic != LIBRARY_MAGIC) return SCE_FONT_ERROR_INVALID_LIBRARY; + if (lib->sys_driver && lib->sys_driver->term) lib->sys_driver->term(lib); + const auto freeFn = lib->iface->dealloc; + void* allocCtx = lib->alloc_ctx; + if ((lib->flags & 1u) != 0 && lib->device_cache_buf) freeFn(allocCtx, lib->device_cache_buf); + if (lib->external_fonts_ctx) freeFn(allocCtx, lib->external_fonts_ctx); + if (lib->sysfonts_ctx) freeFn(allocCtx, lib->sysfonts_ctx); + if (lib->workspace) freeFn(allocCtx, lib->workspace); + lib->magic = 0; + freeFn(allocCtx, lib); + *pLibrary = nullptr; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontCreateRendererWithEdition(const FontMemory* memory, const void* createParams, std::uint64_t edition, FontRenderer* pRenderer) { + if (!memory) { + if (pRenderer) *pRenderer = nullptr; + return SCE_FONT_ERROR_INVALID_PARAMETER; + } + int rc = SCE_FONT_ERROR_INVALID_MEMORY; + if (memory->mem_kind == MEMORY_MAGIC && memory->iface && memory->iface->alloc && memory->iface->dealloc) { + rc = SCE_FONT_ERROR_INVALID_PARAMETER; + if (createParams && pRenderer) { + const auto allocFn = memory->iface->alloc; + const auto freeFn = memory->iface->dealloc; + const auto* selection = static_cast(createParams); + void* rendererMemory = allocFn(memory->mspace_handle, selection->size); + void* workspace = allocFn(memory->mspace_handle, 0x4000); + rc = SCE_FONT_ERROR_ALLOCATION_FAILED; + if (rendererMemory && workspace) { + auto* renderer = static_cast(rendererMemory); + std::memset(renderer, 0, offsetof(RendererNative, mem_kind)); + renderer->magic = RENDERER_MAGIC; + renderer->mem_kind = MEMORY_MAGIC; + renderer->region_size = memory->region_size; + renderer->region_base = memory->region_base; + renderer->alloc_ctx = memory->mspace_handle; + renderer->mem_iface = memory->iface; + renderer->alloc_fn = memory->iface->alloc; + renderer->free_fn = memory->iface->dealloc; + renderer->realloc_fn = memory->iface->realloc_fn; + renderer->calloc_fn = memory->iface->calloc_fn; + renderer->selection = const_cast(createParams); + renderer->outline_magic_0x90 = 0x400000000000ull; + renderer->workspace = workspace; + renderer->workspace_size = 0x4000; + renderer->reserved_a8 = 0; + renderer->outline_policy_flag = 0; + rc = selection->create_fn ? selection->create_fn(renderer) : SCE_FONT_ERROR_FATAL; + if (rc == SCE_FONT_OK) { + if (EffectiveEdition(edition) - 0x930000u < 0x1BD0000u) { + renderer->feature_byte_0x0c |= 1u; + renderer->outline_policy_flag = 1; + } + *pRenderer = renderer; + return SCE_FONT_OK; + } + } + if (workspace) freeFn(memory->mspace_handle, workspace); + if (rendererMemory) freeFn(memory->mspace_handle, rendererMemory); + } + } + if (pRenderer) *pRenderer = nullptr; + return rc; +} + +int APS5_VABI sceFontCreateRenderer(const FontMemory* memory, const void* createParams, FontRenderer* pRenderer) { + return sceFontCreateRendererWithEdition(memory, createParams, 0, pRenderer); +} + +int APS5_VABI sceFontDestroyRenderer(FontRenderer* pRenderer) { + if (!pRenderer) return SCE_FONT_ERROR_INVALID_PARAMETER; + auto* renderer = static_cast(*pRenderer); + if (!renderer || renderer->magic != RENDERER_MAGIC) return SCE_FONT_ERROR_INVALID_RENDERER; + const auto* selection = static_cast(AcquireRendererSelection(renderer)); + int rc = SCE_FONT_ERROR_FATAL; + if (selection && selection->destroy_fn) rc = selection->destroy_fn(renderer); + renderer->selection = nullptr; + const auto freeFn = renderer->free_fn; + void* allocCtx = renderer->alloc_ctx; + if (renderer->workspace) freeFn(allocCtx, renderer->workspace); + freeFn(allocCtx, renderer); + *pRenderer = nullptr; + return rc; +} + +int APS5_VABI sceFontRendererGetOutlineBufferSize(FontRenderer fontRenderer, std::uint32_t* size) { + if (!size) return SCE_FONT_ERROR_INVALID_PARAMETER; + *size = 0; + auto* renderer = static_cast(fontRenderer); + if (!renderer || renderer->magic != RENDERER_MAGIC) return SCE_FONT_ERROR_INVALID_RENDERER; + *size = static_cast(renderer->workspace_size); + return SCE_FONT_OK; +} + +int APS5_VABI sceFontRendererResetOutlineBuffer(FontRenderer fontRenderer) { + auto* renderer = static_cast(fontRenderer); + if (!renderer || renderer->magic != RENDERER_MAGIC) return SCE_FONT_ERROR_INVALID_RENDERER; + if (renderer->workspace && renderer->workspace_size) std::memset(renderer->workspace, 0, static_cast(renderer->workspace_size)); + return SCE_FONT_OK; +} + +int APS5_VABI sceFontRendererSetOutlineBufferPolicy(FontRenderer fontRenderer, std::uint64_t bufferPolicy, std::uint32_t basalSize, std::uint32_t limitSize) { + (void)bufferPolicy; + auto* renderer = static_cast(fontRenderer); + if (!renderer || renderer->magic != RENDERER_MAGIC) return SCE_FONT_ERROR_INVALID_RENDERER; + if (limitSize != 0 && basalSize > limitSize) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (!renderer->alloc_fn || !renderer->free_fn || !renderer->alloc_ctx) return SCE_FONT_ERROR_INVALID_MEMORY; + auto desiredSize = std::max(static_cast(renderer->workspace_size), static_cast(basalSize)); + if (limitSize != 0) desiredSize = std::min(desiredSize, static_cast(limitSize)); + if (desiredSize == 0) desiredSize = 0x4000; + if (!renderer->workspace || renderer->workspace_size != desiredSize) { + void* workspace = renderer->alloc_fn(renderer->alloc_ctx, static_cast(desiredSize)); + if (!workspace) return SCE_FONT_ERROR_ALLOCATION_FAILED; + if (renderer->workspace) renderer->free_fn(renderer->alloc_ctx, renderer->workspace); + renderer->workspace = workspace; + renderer->workspace_size = desiredSize; + } + return SCE_FONT_OK; +} + +int APS5_VABI sceFontAttachDeviceCacheBuffer(FontLibrary library, void* buffer, std::uint32_t size) { + auto* lib = static_cast(library); + if (!lib || lib->magic != LIBRARY_MAGIC) return SCE_FONT_ERROR_INVALID_LIBRARY; + std::uint32_t previous = 0; + if (!AcquireLibraryLock(lib, previous)) return SCE_FONT_ERROR_INVALID_LIBRARY; + std::uint32_t* current = AcquireDeviceCache(lib); + std::uint32_t* stored = current; + int rc; + if (current) { + rc = SCE_FONT_ERROR_ALREADY_ATTACHED; + } else if (size < 0x1020) { + rc = SCE_FONT_ERROR_INVALID_PARAMETER; + } else { + auto* header = static_cast(buffer); + std::uint32_t owned = 0; + if (!header) { + header = static_cast(lib->iface->alloc(lib->alloc_ctx, size)); + owned = 1; + } + if (!header) { + rc = SCE_FONT_ERROR_ALLOCATION_FAILED; + } else { + const std::uint32_t pageCount = (size - 0x1000) >> 12; + header[0] = size; + header[1] = pageCount; + header[2] = 0; + header[3] = pageCount; + const std::uint64_t pageInfo = 0x0FF800001000ull; + std::memcpy(header + 4, &pageInfo, sizeof(pageInfo)); + if (size - 0x1000 > 0x0FFF) { + lib->flags |= owned; + stored = header; + rc = SCE_FONT_OK; + } else { + if (owned) lib->iface->dealloc(lib->alloc_ctx, header); + rc = SCE_FONT_ERROR_INVALID_PARAMETER; + } + } + } + ReleaseDeviceCache(lib, stored); + ReleaseLibraryLock(lib, previous); + return rc; +} + +int APS5_VABI sceFontClearDeviceCache(FontLibrary library) { + auto* lib = static_cast(library); + if (!lib || lib->magic != LIBRARY_MAGIC) return SCE_FONT_ERROR_INVALID_LIBRARY; + std::uint32_t previous = 0; + if (!AcquireLibraryLock(lib, previous)) return SCE_FONT_ERROR_INVALID_LIBRARY; + std::uint32_t* current = AcquireDeviceCache(lib); + int rc = SCE_FONT_ERROR_NOT_ATTACHED_CACHE_BUFFER; + if (current) { + current[3] = current[1]; + current[2] = 0; + rc = SCE_FONT_OK; + } + ReleaseDeviceCache(lib, current); + ReleaseLibraryLock(lib, previous); + return rc; +} + +int APS5_VABI sceFontSupportExternalFonts(FontLibrary library, std::uint32_t fontMax, std::uint32_t formats) { + return SupportFonts(library, &FontLibNative::external_fonts_ctx, fontMax, (fontMax << 6) | 0x20u, formats); +} + +int APS5_VABI sceFontSupportSystemFonts(FontLibrary library) { + return SupportFonts(library, &FontLibNative::sysfonts_ctx, 0x40, 0x1020, 0x52); +} + +int APS5_VABI sceFontSetFontsOpenMode(FontLibrary library, std::uint32_t openMode) { + auto* lib = static_cast(library); + if (!lib || lib->magic != LIBRARY_MAGIC) return SCE_FONT_ERROR_INVALID_LIBRARY; + if (openMode > 2) return SCE_FONT_ERROR_INVALID_PARAMETER; + return SCE_FONT_OK; +} + +} + +#pragma GCC visibility pop diff --git a/core/libs/prx/libSceFont/src/Open.cpp b/core/libs/prx/libSceFont/src/Open.cpp new file mode 100644 index 000000000..b92f7ebf9 --- /dev/null +++ b/core/libs/prx/libSceFont/src/Open.cpp @@ -0,0 +1,509 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "prx/libSceFont/include/FontInternal.hpp" + +namespace { + +using namespace Font; + +constexpr std::array SYSTEM_FONT_SET_TYPES = { + 0x18070043u, 0x18070044u, 0x18070045u, 0x18070047u, 0x18070053u, 0x18070054u, + 0x18070055u, 0x18070057u, 0x180700C3u, 0x180700C4u, 0x180700C5u, 0x180700C7u, + 0x18070444u, 0x18070447u, 0x18070454u, 0x18070457u, 0x180704C4u, 0x180704C7u, + 0x18071053u, 0x18071054u, 0x18071055u, 0x18071057u, 0x18071454u, 0x18071457u, + 0x18072444u, 0x18072447u, 0x180724C4u, 0x180724C7u, 0x18073454u, 0x18073457u, + 0x180734D4u, 0x180734D7u, 0x18078044u, 0x180780C4u, 0x18079054u, 0x1807A044u, + 0x1807A0C4u, 0x1807A444u, 0x1807A4C4u, 0x1807AC44u, 0x1807ACC4u, 0x1807B054u, + 0x1807B0D4u, 0x1807B454u, 0x1807B4D4u, 0x1807BC54u, 0x1807BCD4u, 0x18080444u, + 0x18080447u, 0x18080454u, 0x18080457u, 0x180804C4u, 0x180804C7u, 0x18081454u, + 0x18081457u, 0x18082444u, 0x18082447u, 0x180824C4u, 0x180824C7u, 0x18083454u, + 0x18083457u, 0x180834D4u, 0x180834D7u, 0x1808A444u, 0x1808A4C4u, 0x1808B454u, + 0x1808B4D4u, 0x180C8044u, 0x180C80C4u, 0x180C9054u, 0x180CA044u, 0x180CA0C4u, + 0x180CAC44u, 0x180CACC4u, 0x180CB054u, 0x180CB0D4u, 0x180CBC54u, 0x180CBCD4u, + 0x18170043u, 0x18170044u, 0x18170045u, 0x18170047u, 0x18170444u, 0x18170447u, + 0x18370044u, 0x18370047u, 0x18370444u, 0x18370447u, +}; + +constexpr std::uint16_t HANDLE_OWNED = 0x10; + +struct OpenRequest { + FontLibNative* library; + std::uint32_t modeLow; + std::uint32_t driverMode; + const void* source; + std::uint32_t sourceSize; + bool matchAddress; + std::uint32_t subFontIndex; + std::int32_t uniqueId; +}; + +FontHandle PrepareHandle(FontLibNative* library, FontHandle existing) { + if (existing) { + GetNativeFont(existing)->flags = 0; + RemoveState(existing); + return existing; + } + auto handle = static_cast(library->iface->alloc(library->alloc_ctx, sizeof(FontHandleOpaque))); + if (!handle) return nullptr; + std::memset(handle, 0, sizeof(FontHandleOpaque)); + GetNativeFont(handle)->flags = HANDLE_OWNED; + return handle; +} + +void DiscardHandle(FontLibNative* library, FontHandle handle) { + auto* font = GetNativeFont(handle); + font->magic = 0; + font->open_info = {}; + font->library = library; + const std::uint16_t previousFlags = font->flags; + font->flags = 0; + RemoveState(handle); + if ((previousFlags & HANDLE_OWNED) != 0) library->iface->dealloc(library->alloc_ctx, handle); +} + +void InitializeOpenedHandle(FontHandleNative* font, FontLibNative* library, FontObj* obj, std::uint32_t modeLow, std::uint32_t packedId, std::uint32_t entryIndex, std::uint32_t subFontIndex) { + const SysDriver* driver = library->sys_driver; + font->magic = HANDLE_MAGIC; + font->flags = static_cast(font->flags | modeLow); + font->open_info = {}; + font->open_info.unique_id_packed = packedId; + font->open_info.ctx_entry_index = entryIndex; + font->open_info.sub_font_index = subFontIndex; + font->library = library; + font->renderer = nullptr; + std::memset(font->reserved_0x38, 0, sizeof(font->reserved_0x38)); + std::memset(&font->cached_style, 0, sizeof(font->cached_style)); + std::memset(font->reserved_0xcc, 0, sizeof(font->reserved_0xcc)); + std::memset(font->reserved_0xe8, 0, sizeof(font->reserved_0xe8)); + font->prevFont = nullptr; + font->nextFont = nullptr; + std::uint16_t metric[2] = {}; + driver->metric(obj, 0x0E00, metric); + font->metricA = metric[0]; + driver->metric(obj, 0xEA00, metric); + font->metricB = metric[0]; + float scale = 0.0f; + driver->scale(obj, metric, &scale); + font->style = {}; + font->style.dpi_x = 0x48; + font->style.dpi_y = 0x48; + font->style.scale_w = scale; + font->style.scale_h = scale; +} + +void SetOpenFlags(FontLibNative* library, std::uint32_t modeLow) { + const std::uint32_t flags = library->flags; + const std::uint32_t firstBit = modeLow == 2 ? 0x100000u : 0x200000u; + const std::uint32_t secondBit = modeLow == 2 ? 0x10000u : 0x20000u; + if ((flags & firstBit) != 0) library->sysfont_flags |= 1; + if ((flags & secondBit) != 0) library->sysfont_flags |= 2; +} + +std::uint32_t ClaimExternalEntry(FontCtxHeader* header, const OpenRequest& request) { + AcquireWordLock(header->lock_word); + auto* entries = static_cast(header->base); + std::uint32_t entryIndex = 0xFFFFFFFFu; + if (header->max_entries != 0 && entries) { + const auto uniqueId = static_cast(request.uniqueId); + const auto address = request.matchAddress ? reinterpret_cast(request.source) : 0; + std::uint32_t firstFree = 0xFFFFFFFFu; + for (std::uint32_t i = 0; i < header->max_entries; ++i) { + const auto& entry = entries[i]; + if ((entry.active != 0 && entry.unique_id == uniqueId) || (request.matchAddress && entry.font_address == address)) { + entryIndex = i; + break; + } + if (firstFree == 0xFFFFFFFFu && entry.active == 0) firstFree = i; + } + if (entryIndex == 0xFFFFFFFFu && firstFree != 0xFFFFFFFFu) { + entryIndex = firstFree; + auto& entry = entries[entryIndex]; + entry = {}; + entry.active = 1; + entry.font_address = address; + entry.unique_id = uniqueId != 0xFFFFFFFFu ? uniqueId : (entryIndex ^ 0x80000000u); + } + } + std::atomic_ref(header->lock_word).store(0, std::memory_order_release); + return entryIndex; +} + +int OpenExternalFont(const OpenRequest& request, std::uint32_t libraryLock, FontHandle* pFontHandle) { + FontLibNative* library = request.library; + FontHandle handle = PrepareHandle(library, *pFontHandle); + if (!handle) { + ReleaseLibraryLock(library, libraryLock); + *pFontHandle = nullptr; + return SCE_FONT_ERROR_ALLOCATION_FAILED; + } + const auto fail = [&](int rc) { + DiscardHandle(library, handle); + ReleaseLibraryLock(library, libraryLock); + *pFontHandle = nullptr; + return rc; + }; + auto* header = static_cast(library->external_fonts_ctx); + const std::uint32_t entryIndex = ClaimExternalEntry(header, request); + if (entryIndex == 0xFFFFFFFFu) return fail(SCE_FONT_ERROR_FONT_OPEN_MAX); + const std::uint32_t packedId = request.uniqueId == -1 ? entryIndex + 0x40000000u : (static_cast(request.uniqueId) | 0x80000000u); + FontObj* fontObj = nullptr; + std::uint32_t lockWord = 0; + auto* entry = AcquireFontCtxEntry(header, entryIndex, request.modeLow, &fontObj, &lockWord); + if (!entry) return fail(SCE_FONT_ERROR_FONT_OPEN_MAX); + const SysDriver* driver = library->sys_driver; + if (!driver->open || !driver->metric || !driver->scale) { + ReleaseFontCtxEntryLock(entry, request.modeLow, lockWord); + return fail(SCE_FONT_ERROR_INVALID_LIBRARY); + } + int rc = SCE_FONT_OK; + bool needOpen = (lockWord & OPEN_BIT) == 0; + if (!needOpen) { + rc = SCE_FONT_ERROR_FONT_OPEN_MAX; + if ((lockWord & COUNT_MASK) != COUNT_MASK) { + if (FontObj* node = FindSubFont(fontObj, request.subFontIndex)) { + ++node->refcount; + fontObj = node; + rc = SCE_FONT_OK; + } else { + needOpen = true; + } + } + } + if (needOpen) { + SetOpenFlags(library, request.modeLow); + rc = driver->open(library, request.driverMode, request.source, request.sourceSize, request.subFontIndex, packedId, &fontObj); + library->sysfont_flags = 0; + if (rc == SCE_FONT_OK) { + *EntryObjectSlot(entry, request.modeLow) = fontObj; + lockWord |= OPEN_BIT; + } + } + if (rc == SCE_FONT_OK) { + InitializeOpenedHandle(GetNativeFont(handle), library, fontObj, request.modeLow, packedId, entryIndex, request.subFontIndex); + ++lockWord; + *pFontHandle = handle; + } + ReleaseFontCtxEntryLock(entry, request.modeLow, lockWord); + if (rc != SCE_FONT_OK) return fail(rc); + if ((GetNativeFont(handle)->flags & HANDLE_OWNED) != 0) LinkFontToLibrary(library, handle); + ReleaseLibraryLock(library, libraryLock); + return SCE_FONT_OK; +} + +int BeginOpen(FontLibrary library, FontHandle* pFontHandle, std::uint32_t& libraryLock) { + auto* lib = static_cast(library); + if (!lib || lib->magic != LIBRARY_MAGIC || !AcquireLibraryLock(lib, libraryLock)) { + if (pFontHandle) *pFontHandle = nullptr; + return SCE_FONT_ERROR_INVALID_LIBRARY; + } + int rc = SCE_FONT_OK; + if (!lib->fontset_registry || !lib->sys_driver || !lib->iface || !lib->iface->alloc || !lib->iface->dealloc) { + rc = SCE_FONT_ERROR_INVALID_LIBRARY; + } else if (!lib->external_fonts_ctx) { + rc = SCE_FONT_ERROR_NO_SUPPORT_FUNCTION; + } + if (rc != SCE_FONT_OK) { + ReleaseLibraryLock(lib, libraryLock); + if (pFontHandle) *pFontHandle = nullptr; + } + return rc; +} + +bool ReadFileBytes(const std::filesystem::path& path, std::vector& bytes) { + std::ifstream file(path, std::ios::binary); + if (!file) return false; + bytes.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); + return !file.bad(); +} + +void CopyStyleToCache(FontHandleNative* font) { + std::memcpy(&font->cached_style.state, &font->style, sizeof(StyleStateBlock)); +} + +} + +#pragma GCC visibility push(default) + +extern "C" { + +int APS5_VABI sceFontOpenFontMemory(FontLibrary library, const void* fontAddress, std::uint32_t fontSize, const FontOpenDetail* openDetail, FontHandle* pFontHandle) { + std::uint32_t libraryLock = 0; + const int beginRc = BeginOpen(library, pFontHandle, libraryLock); + if (beginRc != SCE_FONT_OK) return beginRc; + auto* lib = static_cast(library); + const std::uint32_t subFontIndex = openDetail ? openDetail->subfont_index : 0u; + const std::int32_t uniqueId = openDetail ? openDetail->unique_id : -1; + if (!fontAddress || fontSize == 0 || uniqueId < -1 || !pFontHandle) { + ReleaseLibraryLock(lib, libraryLock); + if (pFontHandle) *pFontHandle = nullptr; + return SCE_FONT_ERROR_INVALID_PARAMETER; + } + const OpenRequest request{lib, 1, 1, fontAddress, fontSize, true, subFontIndex, uniqueId}; + const int rc = OpenExternalFont(request, libraryLock, pFontHandle); + if (rc != SCE_FONT_OK) return rc; + const auto* bytes = static_cast(fontAddress); + LoadStateFace(ResetState(*pFontHandle), std::vector(bytes, bytes + fontSize), subFontIndex); + return SCE_FONT_OK; +} + +int APS5_VABI sceFontOpenFontFile(FontLibrary library, const char* path, std::uint32_t openMode, const FontOpenDetail* openDetail, FontHandle* pFontHandle) { + if (!library) { + if (pFontHandle) *pFontHandle = nullptr; + return SCE_FONT_ERROR_INVALID_LIBRARY; + } + if (!pFontHandle) return SCE_FONT_ERROR_INVALID_PARAMETER; + const std::filesystem::path hostPath = path ? ResolvePath_nid_no_patch(path) : std::filesystem::path{}; + const std::string hostPathString = hostPath.string(); + std::uint32_t libraryLock = 0; + const int beginRc = BeginOpen(library, pFontHandle, libraryLock); + if (beginRc != SCE_FONT_OK) return beginRc; + auto* lib = static_cast(library); + const std::uint32_t subFontIndex = openDetail ? openDetail->subfont_index : 0u; + const std::int32_t uniqueId = openDetail ? openDetail->unique_id : -1; + const std::uint32_t modeLow = openMode & 0x0Fu; + if (!path || uniqueId < -1 || modeLow < 1 || modeLow > 3 || hostPathString.empty()) { + ReleaseLibraryLock(lib, libraryLock); + *pFontHandle = nullptr; + return SCE_FONT_ERROR_INVALID_PARAMETER; + } + const OpenRequest request{lib, modeLow, modeLow + 4, hostPathString.c_str(), 0, false, subFontIndex, uniqueId}; + const int rc = OpenExternalFont(request, libraryLock, pFontHandle); + if (rc != SCE_FONT_OK) return rc; + std::vector bytes; + FontState& state = ResetState(*pFontHandle); + if (ReadFileBytes(hostPath, bytes)) LoadStateFace(state, std::move(bytes), subFontIndex); + return SCE_FONT_OK; +} + +int APS5_VABI sceFontOpenFontInstance(FontHandle fontHandle, FontHandle setupFont, FontHandle* pFontHandle) { + if (!fontHandle) { + if (pFontHandle) *pFontHandle = nullptr; + return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + } + if (!setupFont && !pFontHandle) return SCE_FONT_ERROR_INVALID_PARAMETER; + auto* source = GetNativeFont(fontHandle); + std::uint32_t sourceLock = 0; + if (source->magic != HANDLE_MAGIC || !AcquireFontLock(source, sourceLock)) { + if (pFontHandle) *pFontHandle = nullptr; + return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + } + const auto fail = [&](int rc) { + ReleaseFontLock(source, sourceLock); + if (pFontHandle) *pFontHandle = nullptr; + return rc; + }; + auto* library = static_cast(source->library); + if (!library || library->magic != LIBRARY_MAGIC) return fail(SCE_FONT_ERROR_INVALID_FONT_HANDLE); + const std::uint32_t modeLow = source->flags & 0x0Fu; + const std::uint32_t subFontIndex = source->open_info.sub_font_index; + const std::uint32_t entryIndex = source->open_info.ctx_entry_index; + auto* header = static_cast(library->external_fonts_ctx); + if (!header) return fail(SCE_FONT_ERROR_INVALID_FONT_HANDLE); + FontHandle target = setupFont; + bool owned = false; + if (!target) { + if (!library->iface || !library->iface->alloc || !library->iface->dealloc) return fail(SCE_FONT_ERROR_INVALID_FONT_HANDLE); + target = static_cast(library->iface->alloc(library->alloc_ctx, sizeof(FontHandleOpaque))); + if (!target) return fail(SCE_FONT_ERROR_ALLOCATION_FAILED); + owned = true; + } else { + RemoveState(target); + } + std::memcpy(target, fontHandle, sizeof(FontHandleOpaque)); + auto* font = GetNativeFont(target); + font->lock_word = 0; + font->cached_style.cache_lock_word = 0; + font->flags = static_cast((font->flags & 0xFF0Fu) | (owned ? HANDLE_OWNED : 0u)); + auto* entries = static_cast(header->base); + if (!entries || header->max_entries < 1) { + font->magic = 0; + if (owned) library->iface->dealloc(library->alloc_ctx, target); + return fail(SCE_FONT_ERROR_INVALID_PARAMETER); + } + int rc = SCE_FONT_OK; + if (static_cast(entryIndex) >= 0) { + if (entryIndex >= header->max_entries || modeLow < 1 || modeLow > 3) { + rc = SCE_FONT_ERROR_INVALID_PARAMETER; + } else { + FontCtxEntry* entry = entries + entryIndex; + std::uint32_t* lockWord = EntryLockWord(entry, modeLow); + const std::uint32_t previous = AcquireWordLock(*lockWord); + FontObj* node = FindSubFont(static_cast(*EntryObjectSlot(entry, modeLow)), subFontIndex); + std::uint32_t updated = previous; + if ((previous & OPEN_BIT) == 0) { + rc = SCE_FONT_ERROR_INVALID_FONT_HANDLE; + } else if ((previous & COUNT_MASK) == COUNT_MASK) { + rc = SCE_FONT_ERROR_FONT_OPEN_MAX; + } else if (!node) { + rc = SCE_FONT_ERROR_FATAL; + } else { + ++node->refcount; + ++updated; + } + std::atomic_ref(*lockWord).store(updated & ~LOCK_BIT, std::memory_order_release); + } + } + if (rc != SCE_FONT_OK) { + font->magic = 0; + font->open_info = {}; + font->library = library; + const std::uint16_t previousFlags = font->flags; + font->flags = 0; + if ((previousFlags & HANDLE_OWNED) != 0) library->iface->dealloc(library->alloc_ctx, target); + RemoveState(target); + return fail(rc); + } + if (owned) LinkFontToLibrary(library, target); + ReleaseFontLock(source, sourceLock); + FontState& targetState = ResetState(target); + if (FontState* sourceState = TryGetState(fontHandle)) { + targetState.scaleW = sourceState->scaleW; + targetState.scaleH = sourceState->scaleH; + if (!sourceState->faceData.empty()) LoadStateFace(targetState, sourceState->faceData, subFontIndex); + } + if (pFontHandle) *pFontHandle = target; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontOpenFontSet(FontLibrary library, std::uint32_t fontSetType, std::uint32_t openMode, const FontOpenDetail* openDetail, FontHandle* pFontHandle) { + (void)openDetail; + auto* lib = static_cast(library); + if (!lib || lib->magic != LIBRARY_MAGIC) { + if (pFontHandle) *pFontHandle = nullptr; + return SCE_FONT_ERROR_INVALID_LIBRARY; + } + if (!pFontHandle) return SCE_FONT_ERROR_INVALID_PARAMETER; + std::uint32_t libraryLock = 0; + if (!AcquireLibraryLock(lib, libraryLock)) { + *pFontHandle = nullptr; + return SCE_FONT_ERROR_INVALID_LIBRARY; + } + int rc; + if (openMode != 1 && openMode != 2 && openMode != 3) { + rc = SCE_FONT_ERROR_INVALID_PARAMETER; + } else if (!lib->fontset_registry || !lib->sys_driver) { + rc = SCE_FONT_ERROR_INVALID_LIBRARY; + } else if (!lib->iface || !lib->iface->alloc || !lib->iface->dealloc) { + rc = SCE_FONT_ERROR_INVALID_MEMORY; + } else if (!lib->sysfonts_ctx) { + rc = SCE_FONT_ERROR_NO_SUPPORT_FUNCTION; + } else if (std::find(SYSTEM_FONT_SET_TYPES.begin(), SYSTEM_FONT_SET_TYPES.end(), fontSetType) == SYSTEM_FONT_SET_TYPES.end()) { + rc = SCE_FONT_ERROR_NO_SUPPORT_FONTSET; + } else { + rc = SCE_FONT_ERROR_FONT_OPEN_FAILED; + } + ReleaseLibraryLock(lib, libraryLock); + *pFontHandle = nullptr; + return rc; +} + +int APS5_VABI sceFontCloseFont(FontHandle fontHandle) { + auto* font = GetNativeFont(fontHandle); + std::uint32_t fontLock = 0; + if (!font || font->magic != HANDLE_MAGIC || !AcquireFontLock(font, fontLock)) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + auto* library = static_cast(font->library); + std::uint32_t libraryLock = 0; + if (!library || library->magic != LIBRARY_MAGIC || !AcquireLibraryLock(library, libraryLock)) { + ReleaseFontLock(font, fontLock); + return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + } + ReleaseFontObjectsForHandle(font); + const std::uint16_t flags = font->flags; + if ((flags & HANDLE_OWNED) != 0) UnlinkFontFromLibrary(library, fontHandle); + RemoveState(fontHandle); + font->magic = 0; + font->open_info = {}; + font->renderer = nullptr; + font->flags = 0; + font->lock_word = 0; + if ((flags & HANDLE_OWNED) != 0) library->iface->dealloc(library->alloc_ctx, fontHandle); + ReleaseLibraryLock(library, libraryLock); + return SCE_FONT_OK; +} + +int APS5_VABI sceFontGetLibrary(FontHandle fontHandle, FontLibrary* pLibrary) { + if (!pLibrary) return SCE_FONT_ERROR_INVALID_PARAMETER; + const auto* font = GetNativeFont(fontHandle); + if (!font || font->magic != HANDLE_MAGIC) { + *pLibrary = nullptr; + return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + } + *pLibrary = font->library; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontBindRenderer(FontHandle fontHandle, FontRenderer renderer) { + auto* font = GetNativeFont(fontHandle); + std::uint32_t fontLock = 0; + if (!font || !AcquireFontLock(font, fontLock)) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + std::uint32_t cachedLock = 0; + if (!AcquireCachedStyleLock(font, cachedLock)) { + ReleaseFontLock(font, fontLock); + return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + } + int rc = SCE_FONT_ERROR_ALREADY_BOUND_RENDERER; + if (!font->renderer) { + rc = SCE_FONT_ERROR_INVALID_RENDERER; + if (renderer && static_cast(renderer)->magic == RENDERER_MAGIC) { + CopyStyleToCache(font); + font->renderer = renderer; + rc = SCE_FONT_OK; + } + } + ReleaseCachedStyleLock(font, cachedLock); + ReleaseFontLock(font, fontLock); + return rc; +} + +int APS5_VABI sceFontRebindRenderer(FontHandle fontHandle) { + auto* font = GetNativeFont(fontHandle); + if (!font || font->magic != HANDLE_MAGIC) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + std::uint32_t fontLock = 0; + if (!AcquireFontLock(font, fontLock)) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + std::uint32_t cachedLock = 0; + if (!AcquireCachedStyleLock(font, cachedLock)) { + ReleaseFontLock(font, fontLock); + return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + } + int rc; + if (!font->renderer) { + rc = SCE_FONT_ERROR_NOT_BOUND_RENDERER; + } else if (static_cast(font->renderer)->magic != RENDERER_MAGIC) { + rc = SCE_FONT_ERROR_FATAL; + } else { + CopyStyleToCache(font); + rc = SCE_FONT_OK; + } + ReleaseCachedStyleLock(font, cachedLock); + ReleaseFontLock(font, fontLock); + return rc; +} + +int APS5_VABI sceFontUnbindRenderer(FontHandle fontHandle) { + auto* font = GetNativeFont(fontHandle); + std::uint32_t cachedLock = 0; + if (!font || !AcquireCachedStyleLock(font, cachedLock)) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + int rc = SCE_FONT_ERROR_NOT_BOUND_RENDERER; + if (font->renderer) { + font->renderer = nullptr; + rc = SCE_FONT_OK; + } + ReleaseCachedStyleLock(font, cachedLock); + return rc; +} + +} + +#pragma GCC visibility pop diff --git a/core/libs/prx/libSceFont/src/Render.cpp b/core/libs/prx/libSceFont/src/Render.cpp new file mode 100644 index 000000000..74cdde106 --- /dev/null +++ b/core/libs/prx/libSceFont/src/Render.cpp @@ -0,0 +1,449 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include + +#include +#include FT_FREETYPE_H + +#include "prx/libSceFont/include/FontInternal.hpp" + +namespace { + +using namespace Font; + +constexpr std::uint16_t GENERATE_GLYPH_DETAIL_ID = 0x0FD3; + +std::uint16_t ClampToU16(float value) { + if (value <= 0.0f) return 0; + return static_cast(std::lround(std::min(value, static_cast(std::numeric_limits::max())))); +} + +std::uint8_t* LayoutCacheBytes(FontHandleNative* font) { + return reinterpret_cast(&font->cached_style) + offsetof(CachedStyle, layout_cache_bytes); +} + +RenderSurfaceSystemUse* SurfaceSystemUse(FontRenderSurface* surface) { + return reinterpret_cast(surface->reserved_q); +} + +bool SurfaceScaleFrame(FontRenderSurface* surface, StyleStateBlock& state, int& rc) { + if (!surface || (surface->styleFlag & 0x1) == 0) return false; + const FontStyleFrame* frame = SurfaceSystemUse(surface)->styleframe; + if (!frame || (frame->flags1 & STYLE_FRAME_FLAG_SCALE) == 0) return false; + state = {}; + state.dpi_x = frame->hDpi; + state.dpi_y = frame->vDpi; + if ((frame->flags1 & STYLE_FRAME_FLAG_SLANT) != 0) state.slant_ratio = frame->slantRatio; + if ((frame->flags1 & STYLE_FRAME_FLAG_WEIGHT) != 0) { + state.effect_weight_x = frame->effectWeightX; + state.effect_weight_y = frame->effectWeightY; + } + StyleStateBlock frameScale{}; + frameScale.dpi_x = frame->hDpi; + frameScale.dpi_y = frame->vDpi; + frameScale.scale_unit = frame->scaleUnit; + frameScale.scale_w = frame->scalePixelW; + frameScale.scale_h = frame->scalePixelH; + rc = StyleStateGetScalePixel(&frameScale, &state.scale_w, &state.scale_h); + return true; +} + +int CachedBaseline(FontHandle handle, FontHandleNative* font, float& baseline) { + int rc = SCE_FONT_OK; + const std::uint8_t flags = CachedStyleCacheFlags(font->cached_style); + if ((flags & 0x1) == 0) { + rc = ComputeHorizontalLayout(handle, &font->cached_style.state, LayoutCacheBytes(font)); + if (rc == SCE_FONT_OK) CachedStyleSetCacheFlags(font->cached_style, static_cast(flags | 0x1)); + } + if (rc == SCE_FONT_OK) baseline = LoadFloat(LayoutCacheBytes(font), HORIZONTAL_BASELINE); + return rc; +} + +int CachedColumnOffset(FontHandle handle, FontHandleNative* font, float& offset) { + int rc = SCE_FONT_OK; + const std::uint8_t flags = CachedStyleCacheFlags(font->cached_style); + if ((flags & 0x2) == 0) { + std::uint8_t layout[VERTICAL_LAYOUT_SIZE] = {}; + rc = ComputeVerticalLayout(handle, &font->cached_style.state, layout); + if (rc == SCE_FONT_OK) { + offset = LoadFloat(layout, VERTICAL_BASELINE_OFFSET_X); + CachedStyleSetScalar(font->cached_style, offset); + CachedStyleSetCacheFlags(font->cached_style, static_cast(flags | 0x2)); + } + } + if (rc == SCE_FONT_OK && (CachedStyleCacheFlags(font->cached_style) & 0x2) != 0) offset = CachedStyleGetScalar(font->cached_style); + return rc; +} + +int RenderDirectional(FontHandle fontHandle, std::uint32_t code, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result, std::uint16_t direction) { + auto* font = GetNativeFont(fontHandle); + if (!font || font->magic != HANDLE_MAGIC) { + ClearRenderOutputs(metrics, result); + return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + } + if (code == 0) { + ClearRenderOutputs(metrics, result); + return SCE_FONT_ERROR_NO_SUPPORT_CODE; + } + if (!surface || !metrics || !result) { + ClearRenderOutputs(metrics, result); + return SCE_FONT_ERROR_INVALID_PARAMETER; + } + std::uint32_t fontLock = 0; + if (!AcquireFontLock(font, fontLock)) { + ClearRenderOutputs(metrics, result); + return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + } + CachedStyleSetDirectionWord(font->cached_style, direction); + const int rc = RenderCharGlyphImageCore(fontHandle, code, surface, x, y, metrics, result); + ReleaseFontLock(font, fontLock); + if (rc != SCE_FONT_OK) ClearRenderOutputs(metrics, result); + return rc; +} + +} + +#pragma GCC visibility push(default) + +extern "C" { + +int APS5_VABI sceFontGetCharGlyphMetrics(FontHandle fontHandle, std::uint32_t code, FontGlyphMetrics* metrics) { + return GetCharGlyphMetrics(fontHandle, code, metrics, false); +} + +int APS5_VABI sceFontGetRenderCharGlyphMetrics(FontHandle fontHandle, std::uint32_t code, FontGlyphMetrics* metrics) { + return GetCharGlyphMetrics(fontHandle, code, metrics, true); +} + +int APS5_VABI sceFontGetHorizontalLayout(FontHandle fontHandle, FontHorizontalLayout* layout) { + auto* font = GetNativeFont(fontHandle); + std::uint32_t fontLock = 0; + if (!font || font->magic != HANDLE_MAGIC || !AcquireFontLock(font, fontLock)) { + if (layout) *layout = {}; + return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + } + if (!layout) { + ReleaseFontLock(font, fontLock); + return SCE_FONT_ERROR_INVALID_PARAMETER; + } + std::uint8_t blocks[HORIZONTAL_LAYOUT_SIZE] = {}; + const int rc = ComputeHorizontalLayout(fontHandle, &font->style, blocks); + ReleaseFontLock(font, fontLock); + if (rc != SCE_FONT_OK) { + *layout = {}; + return rc; + } + layout->baselineOffset = LoadFloat(blocks, HORIZONTAL_BASELINE); + layout->lineAdvance = LoadFloat(blocks, HORIZONTAL_LINE_ADVANCE); + layout->decorationExtent = LoadFloat(blocks, HORIZONTAL_EFFECT_HEIGHT); + return SCE_FONT_OK; +} + +int APS5_VABI sceFontGetVerticalLayout(FontHandle fontHandle, FontVerticalLayout* layout) { + int rc = SCE_FONT_ERROR_INVALID_FONT_HANDLE; + auto* font = GetNativeFont(fontHandle); + std::uint32_t fontLock = 0; + if (font && font->magic == HANDLE_MAGIC && AcquireFontLock(font, fontLock)) { + if (!layout) { + rc = SCE_FONT_ERROR_INVALID_PARAMETER; + } else { + std::uint8_t blocks[VERTICAL_LAYOUT_SIZE] = {}; + rc = ComputeVerticalLayout(fontHandle, &font->style, blocks); + if (rc == SCE_FONT_OK) { + layout->baselineOffsetX = LoadFloat(blocks, VERTICAL_BASELINE_OFFSET_X); + layout->columnAdvance = LoadFloat(blocks, VERTICAL_COLUMN_ADVANCE); + layout->decorationSpan = LoadFloat(blocks, VERTICAL_DECORATION_SPAN); + } + } + ReleaseFontLock(font, fontLock); + if (rc == SCE_FONT_OK) return rc; + } + if (layout) *layout = {}; + return rc; +} + +int APS5_VABI sceFontGetKerning(FontHandle fontHandle, std::uint32_t preCode, std::uint32_t code, FontKerning* kerning) { + if (!kerning) return SCE_FONT_ERROR_INVALID_PARAMETER; + auto* font = GetNativeFont(fontHandle); + std::uint32_t fontLock = 0; + if (!font || font->magic != HANDLE_MAGIC || !AcquireFontLock(font, fontLock)) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + *kerning = {}; + const FontState* state = TryGetState(fontHandle); + const FT_Face face = state ? state->face : nullptr; + const FT_UInt previousGlyph = face ? FT_Get_Char_Index(face, preCode) : 0; + const FT_UInt glyph = face ? FT_Get_Char_Index(face, code) : 0; + if (face && previousGlyph != 0 && glyph != 0) { + const auto charW = static_cast(static_cast(state->scaleW * 64.0f)); + const auto charH = static_cast(static_cast(state->scaleH * 64.0f)); + FT_Set_Char_Size(face, charW, charH, 72, 72); + FT_Vector delta{}; + FT_Get_Kerning(face, previousGlyph, glyph, FT_KERNING_DEFAULT, &delta); + kerning->offsetX = static_cast(delta.x) / 64.0f; + } + ReleaseFontLock(font, fontLock); + return SCE_FONT_OK; +} + +int APS5_VABI sceFontRenderCharGlyphImage(FontHandle fontHandle, std::uint32_t code, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result) { + auto* font = GetNativeFont(fontHandle); + std::uint32_t fontLock = 0; + if (!font || font->magic != HANDLE_MAGIC || !AcquireFontLock(font, fontLock)) { + ClearRenderOutputs(metrics, result); + return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + } + int preRc = SCE_FONT_OK; + StyleStateBlock frameState{}; + const bool horizontal = static_cast(font->flags) >= 0; + float xUsed = x; + float yUsed = y; + if (horizontal) { + float baseline = 0.0f; + if (!SurfaceScaleFrame(surface, frameState, preRc)) { + preRc = CachedBaseline(fontHandle, font, baseline); + } else { + std::uint8_t layout[HORIZONTAL_LAYOUT_SIZE] = {}; + if (preRc == SCE_FONT_OK) preRc = ComputeHorizontalLayout(fontHandle, &frameState, layout); + if (preRc == SCE_FONT_OK) { + baseline = LoadFloat(layout, HORIZONTAL_BASELINE); + SurfaceSystemUse(surface)->catchedScale = baseline; + } + } + yUsed = y + baseline; + CachedStyleSetDirectionWord(font->cached_style, 1); + } else { + float offset = 0.0f; + if (!SurfaceScaleFrame(surface, frameState, preRc)) { + preRc = CachedColumnOffset(fontHandle, font, offset); + } else { + std::uint8_t layout[VERTICAL_LAYOUT_SIZE] = {}; + if (preRc == SCE_FONT_OK) preRc = ComputeVerticalLayout(fontHandle, &frameState, layout); + if (preRc == SCE_FONT_OK) { + offset = LoadFloat(layout, VERTICAL_BASELINE_OFFSET_X); + SurfaceSystemUse(surface)->catchedScale = offset; + } else { + preRc = CachedColumnOffset(fontHandle, font, offset); + } + } + xUsed = x + offset; + CachedStyleSetDirectionWord(font->cached_style, 2); + } + int rc; + if (code == 0) { + rc = SCE_FONT_ERROR_NO_SUPPORT_CODE; + } else if (!surface || !metrics || !result) { + rc = SCE_FONT_ERROR_INVALID_PARAMETER; + } else if (preRc != SCE_FONT_OK) { + rc = preRc; + } else { + rc = RenderCharGlyphImageCore(fontHandle, code, surface, xUsed, yUsed, metrics, result); + } + ReleaseFontLock(font, fontLock); + if (rc != SCE_FONT_OK) ClearRenderOutputs(metrics, result); + return rc; +} + +int APS5_VABI sceFontRenderCharGlyphImageHorizontal(FontHandle fontHandle, std::uint32_t code, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result) { + return RenderDirectional(fontHandle, code, surface, x, y, metrics, result, 1); +} + +int APS5_VABI sceFontRenderCharGlyphImageVertical(FontHandle fontHandle, std::uint32_t code, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result) { + return RenderDirectional(fontHandle, code, surface, x, y, metrics, result, 2); +} + +void APS5_VABI sceFontRenderSurfaceInit(FontRenderSurface* renderSurface, void* buffer, int bufWidthByte, int pixelSizeByte, int widthPixel, int heightPixel) { + if (!renderSurface) return; + const auto width = static_cast(std::max(widthPixel, 0)); + const auto height = static_cast(std::max(heightPixel, 0)); + renderSurface->buffer = buffer; + renderSurface->widthByte = bufWidthByte; + renderSurface->pixelSizeByte = static_cast(pixelSizeByte); + renderSurface->pad0 = 0; + renderSurface->styleFlag = 0; + renderSurface->pad2 = 0; + renderSurface->width = static_cast(width); + renderSurface->height = static_cast(height); + renderSurface->sc_x0 = 0; + renderSurface->sc_y0 = 0; + renderSurface->sc_x1 = width; + renderSurface->sc_y1 = height; +} + +void APS5_VABI sceFontRenderSurfaceSetScissor(FontRenderSurface* renderSurface, int x0, int y0, int w, int h) { + if (!renderSurface) return; + const auto surfaceW = static_cast(renderSurface->width); + if (surfaceW != 0) { + std::uint32_t x1; + std::uint32_t left; + auto width = static_cast(w); + if (x0 < 0) { + x1 = width + static_cast(x0); + if (surfaceW < x1) x1 = surfaceW; + if (width <= static_cast(-x0)) x1 = 0; + left = 0; + } else { + x1 = surfaceW; + left = surfaceW; + if (static_cast(x0) <= surfaceW) { + if (surfaceW < width) width = surfaceW; + x1 = width + static_cast(x0); + left = static_cast(x0); + if (surfaceW < x1) x1 = surfaceW; + } + } + renderSurface->sc_x0 = left; + renderSurface->sc_x1 = x1; + } + const auto surfaceH = static_cast(renderSurface->height); + if (surfaceH == 0) return; + std::uint32_t top; + std::uint32_t y1 = surfaceH; + auto height = static_cast(h); + if (y0 < 0) { + top = 0; + if (height <= static_cast(-y0)) { + renderSurface->sc_y0 = 0; + renderSurface->sc_y1 = 0; + return; + } + } else { + if (surfaceH < static_cast(y0)) { + renderSurface->sc_y0 = surfaceH; + renderSurface->sc_y1 = y1; + return; + } + top = static_cast(y0); + if (surfaceH < height) height = surfaceH; + } + const std::uint32_t candidate = height + static_cast(y0); + if (candidate <= surfaceH) y1 = candidate; + renderSurface->sc_y0 = top; + renderSurface->sc_y1 = y1; +} + +int APS5_VABI sceFontRenderSurfaceSetStyleFrame(FontRenderSurface* renderSurface, FontStyleFrame* styleFrame) { + if (!renderSurface) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (!styleFrame) { + renderSurface->styleFlag &= static_cast(~0x1u); + renderSurface->reserved_q[0] = 0; + renderSurface->reserved_q[1] = 0; + return SCE_FONT_OK; + } + if (styleFrame->magic != STYLE_FRAME_MAGIC) return SCE_FONT_ERROR_INVALID_PARAMETER; + renderSurface->styleFlag |= 0x1; + renderSurface->reserved_q[0] = reinterpret_cast(styleFrame); + renderSurface->reserved_q[1] = 0; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontGenerateCharGlyph(FontHandle fontHandle, std::uint32_t code, const FontGenerateGlyphDetail* detail, FontGlyph* pGlyph) { + if (!pGlyph) return SCE_FONT_ERROR_INVALID_PARAMETER; + *pGlyph = nullptr; + if (!fontHandle) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + if (code == 0) return SCE_FONT_ERROR_NO_SUPPORT_CODE; + const FontState* state = TryGetState(fontHandle); + if (!state) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + const std::uint8_t glyphForm = detail ? detail->glyph_form : 0; + const std::uint8_t metricsForm = detail ? detail->metrics_form : 0; + const std::uint16_t formOptions = detail ? detail->form_options : 0; + const FontMemory* glyphMemory = detail ? detail->mem : nullptr; + if (detail && detail->id != GENERATE_GLYPH_DETAIL_ID) return SCE_FONT_ERROR_INVALID_PARAMETER; + if ((formOptions & static_cast(~0x11u)) != 0) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (glyphForm == 0 && metricsForm != 0) return SCE_FONT_ERROR_INVALID_PARAMETER; + if ((glyphForm != 0 && metricsForm == 0) || glyphForm > 1 || metricsForm > 4) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (glyphMemory && glyphMemory->mem_kind != MEMORY_MAGIC) return SCE_FONT_ERROR_INVALID_PARAMETER; + FontGlyphMetrics metrics{}; + const int rc = GetCharGlyphMetrics(fontHandle, code, &metrics, false); + if (rc != SCE_FONT_OK) return rc; + auto* generated = new (std::nothrow) GeneratedGlyph(); + if (!generated) return SCE_FONT_ERROR_ALLOCATION_FAILED; + generated->codepoint = code; + generated->metrics = metrics; + generated->glyph.magic = GLYPH_MAGIC; + generated->glyph.flags = formOptions; + generated->glyph.glyph_form = glyphForm; + generated->glyph.metrics_form = metricsForm; + generated->glyph.em_size = ClampToU16(state->scaleH); + generated->glyph.baseline = ClampToU16(metrics.Horizontal.bearingY); + generated->glyph.height_px = ClampToU16(metrics.height); + generated->glyph.origin_x = 0; + generated->glyph.origin_y = generated->glyph.baseline; + generated->glyph.scale_x = state->scaleW; + generated->glyph.base_scale = state->scaleH; + generated->glyph.memory = glyphMemory; + generated->owner = fontHandle; + generated->outline.outline_flags = generated->glyph.flags; + TrackGeneratedGlyph(&generated->glyph); + *pGlyph = &generated->glyph; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontDeleteGlyph(const FontMemory* memory, FontGlyph* pGlyph) { + (void)memory; + if (!pGlyph) return SCE_FONT_ERROR_INVALID_PARAMETER; + const FontGlyph glyph = *pGlyph; + if (!glyph || glyph->magic != GLYPH_MAGIC || !ForgetGeneratedGlyph(glyph)) return SCE_FONT_ERROR_INVALID_GLYPH; + delete reinterpret_cast(glyph); + *pGlyph = nullptr; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontGlyphGetGlyphForm(FontGlyph glyph) { + if (!glyph || glyph->magic != GLYPH_MAGIC) return SCE_FONT_ERROR_INVALID_GLYPH; + return glyph->glyph_form; +} + +int APS5_VABI sceFontGlyphGetMetricsForm(FontGlyph glyph) { + if (!glyph || glyph->magic != GLYPH_MAGIC) return SCE_FONT_ERROR_INVALID_GLYPH; + return glyph->metrics_form; +} + +int APS5_VABI sceFontGlyphGetScalePixel(FontGlyph glyph, float* w, float* h) { + if (!glyph || glyph->magic != GLYPH_MAGIC || (!w && !h)) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (w) *w = glyph->scale_x; + if (h) *h = glyph->base_scale; + return SCE_FONT_OK; +} + +const FontGlyphMetrics* APS5_VABI sceFontGlyphRefersMetrics(FontGlyph glyph) { + auto* generated = TryGetGeneratedGlyph(glyph); + return generated ? &generated->metrics : nullptr; +} + +const FontGlyphMetricsHorizontal* APS5_VABI sceFontGlyphRefersMetricsHorizontal(FontGlyph glyph) { + auto* generated = TryGetGeneratedGlyph(glyph); + if (!generated) return nullptr; + PopulateGlyphMetricVariants(*generated); + return &generated->metricsHorizontal; +} + +const FontGlyphMetricsHorizontalAdvance* APS5_VABI sceFontGlyphRefersMetricsHorizontalAdvance(FontGlyph glyph) { + auto* generated = TryGetGeneratedGlyph(glyph); + if (!generated) return nullptr; + PopulateGlyphMetricVariants(*generated); + return &generated->metricsHorizontalAdvance; +} + +const FontGlyphMetricsHorizontalX* APS5_VABI sceFontGlyphRefersMetricsHorizontalX(FontGlyph glyph) { + auto* generated = TryGetGeneratedGlyph(glyph); + if (!generated) return nullptr; + PopulateGlyphMetricVariants(*generated); + return &generated->metricsHorizontalX; +} + +FontGlyphOutline* APS5_VABI sceFontGlyphRefersOutline(FontGlyph glyph) { + if (!glyph || glyph->magic != GLYPH_MAGIC || glyph->glyph_form != 1) return nullptr; + auto* generated = TryGetGeneratedGlyph(glyph); + if (!generated) return nullptr; + if (!generated->outlineInitialized && !BuildTrueOutline(*generated)) BuildBoundingOutline(*generated); + return &generated->outline; +} + +} + +#pragma GCC visibility pop diff --git a/core/libs/prx/libSceFont/src/State.cpp b/core/libs/prx/libSceFont/src/State.cpp new file mode 100644 index 000000000..58b7e0e49 --- /dev/null +++ b/core/libs/prx/libSceFont/src/State.cpp @@ -0,0 +1,433 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "prx/libSceFont/include/FontInternal.hpp" + +namespace { + +std::mutex stateMutex; +std::unordered_map> fontStates; +std::mutex glyphMutex; +std::unordered_set generatedGlyphs; + +FT_Library HostFreeTypeLibrary() { + static const FT_Library library = [] { + FT_Library created = nullptr; + if (FT_Init_FreeType(&created) != 0) return static_cast(nullptr); + return created; + }(); + return library; +} + +bool AcquireMagicLock(std::uint32_t& word, const std::uint16_t& magic, std::uint16_t expectedMagic, std::uint32_t& previous) { + for (;;) { + const std::uint32_t current = std::atomic_ref(word).load(std::memory_order_acquire); + if (magic != expectedMagic) return false; + if (static_cast(current) >= 0) { + std::uint32_t expected = current; + if (std::atomic_ref(word).compare_exchange_weak(expected, current | Font::LOCK_BIT, std::memory_order_acq_rel)) { + previous = current; + return true; + } + } + Font::Backoff(); + } +} + +void ReleaseWord(std::uint32_t& word, std::uint32_t previous) { + std::atomic_ref(word).store(previous & ~Font::LOCK_BIT, std::memory_order_release); +} + +} + +Font::FontState::~FontState() { + if (face) FT_Done_Face(face); +} + +void Font::Backoff() { + std::this_thread::sleep_for(std::chrono::microseconds(30)); +} + +std::uint32_t Font::AcquireWordLock(std::uint32_t& word) { + for (;;) { + const std::uint32_t current = std::atomic_ref(word).load(std::memory_order_acquire); + if (static_cast(current) >= 0) { + std::uint32_t expected = current; + if (std::atomic_ref(word).compare_exchange_weak(expected, current | LOCK_BIT, std::memory_order_acq_rel)) return current; + } + Backoff(); + } +} + +Font::FontHandleNative* Font::GetNativeFont(FontHandle handle) { + return reinterpret_cast(handle); +} + +bool Font::AcquireLibraryLock(FontLibNative* library, std::uint32_t& previous) { + return library && AcquireMagicLock(library->lock_word, library->magic, LIBRARY_MAGIC, previous); +} + +void Font::ReleaseLibraryLock(FontLibNative* library, std::uint32_t previous) { + if (library) ReleaseWord(library->lock_word, previous); +} + +bool Font::AcquireFontLock(FontHandleNative* font, std::uint32_t& previous) { + return font && AcquireMagicLock(font->lock_word, font->magic, HANDLE_MAGIC, previous); +} + +void Font::ReleaseFontLock(FontHandleNative* font, std::uint32_t previous) { + if (font) ReleaseWord(font->lock_word, previous); +} + +bool Font::AcquireCachedStyleLock(FontHandleNative* font, std::uint32_t& previous) { + return font && AcquireMagicLock(font->cached_style.cache_lock_word, font->magic, HANDLE_MAGIC, previous); +} + +void Font::ReleaseCachedStyleLock(FontHandleNative* font, std::uint32_t previous) { + if (font) ReleaseWord(font->cached_style.cache_lock_word, previous); +} + +namespace { + +FontHandle* AcquireFontList(Font::FontLibNative* library) { + auto* const busy = reinterpret_cast(std::numeric_limits::max()); + std::atomic_ref slot(library->list_head_ptr); + for (;;) { + FontHandle* current = slot.load(std::memory_order_acquire); + if (current != busy && slot.compare_exchange_weak(current, busy, std::memory_order_acq_rel)) return current; + Font::Backoff(); + } +} + +void ReleaseFontList(Font::FontLibNative* library, FontHandle* head) { + std::atomic_ref(library->list_head_ptr).store(head, std::memory_order_release); +} + +} + +void Font::LinkFontToLibrary(FontLibNative* library, FontHandle handle) { + FontHandle* head = AcquireFontList(library); + if (head) { + const FontHandle oldHead = *head; + auto* font = GetNativeFont(handle); + font->prevFont = nullptr; + font->nextFont = oldHead; + if (oldHead) GetNativeFont(oldHead)->prevFont = handle; + *head = handle; + } + ReleaseFontList(library, head); +} + +void Font::UnlinkFontFromLibrary(FontLibNative* library, FontHandle handle) { + FontHandle* head = AcquireFontList(library); + auto* font = GetNativeFont(handle); + if (head) { + if (font->prevFont) { + GetNativeFont(font->prevFont)->nextFont = font->nextFont; + } else if (*head == handle) { + *head = font->nextFont; + } + if (font->nextFont) GetNativeFont(font->nextFont)->prevFont = font->prevFont; + } + font->prevFont = nullptr; + font->nextFont = nullptr; + ReleaseFontList(library, head); +} + +std::uint32_t* Font::EntryLockWord(FontCtxEntry* entry, std::uint32_t modeLow) { + if (modeLow == 3) return &entry->lock_mode3; + if (modeLow == 1) return &entry->lock_mode1; + return &entry->lock_mode2; +} + +void** Font::EntryObjectSlot(FontCtxEntry* entry, std::uint32_t modeLow) { + if (modeLow == 3) return &entry->obj_mode3; + if (modeLow == 1) return &entry->obj_mode1; + return &entry->obj_mode2; +} + +Font::FontCtxEntry* Font::AcquireFontCtxEntry(void* ctx, std::uint32_t index, std::uint32_t modeLow, FontObj** outObject, std::uint32_t* outLockWord) { + if (!ctx || !outObject || !outLockWord) return nullptr; + auto* header = static_cast(ctx); + if (index >= header->max_entries || !header->base) { + *outLockWord = 0; + *outObject = nullptr; + return nullptr; + } + auto* entry = static_cast(header->base) + index; + if (modeLow != 1 && modeLow != 2 && modeLow != 3) { + *outLockWord = 0; + *outObject = nullptr; + return entry; + } + *outLockWord = AcquireWordLock(*EntryLockWord(entry, modeLow)) | LOCK_BIT; + *outObject = static_cast(*EntryObjectSlot(entry, modeLow)); + return entry; +} + +void Font::ReleaseFontCtxEntryLock(FontCtxEntry* entry, std::uint32_t modeLow, std::uint32_t lockWord) { + if (!entry) return; + if ((lockWord & COUNT_MASK) == 0) *EntryObjectSlot(entry, modeLow) = nullptr; + if ((lockWord & COUNT_MASK) == 0 && !entry->obj_mode1 && !entry->obj_mode3 && !entry->obj_mode2) { + entry->unique_id = 0; + entry->active = 0; + } + ReleaseWord(*EntryLockWord(entry, modeLow), lockWord); +} + +Font::FontObj* Font::FindSubFont(FontObj* head, std::uint32_t subFontIndex) { + for (auto* node = head; node; node = node->next) { + if (node->sub_font_index == subFontIndex) return node; + } + return nullptr; +} + +void Font::ReleaseFontObjectsForHandle(FontHandleNative* font) { + auto* library = static_cast(font->library); + if (!library || library->magic != LIBRARY_MAGIC || !library->sys_driver || !library->sys_driver->close) return; + const std::uint32_t fontId = font->open_info.ctx_entry_index; + if (static_cast(fontId) < 0) return; + const std::uint32_t modeLow = font->flags & 0x0Fu; + FontObj* head = nullptr; + std::uint32_t lockWord = 0; + auto* entry = AcquireFontCtxEntry(library->external_fonts_ctx, fontId, modeLow, &head, &lockWord); + if (!entry) return; + FontObj* match = FindSubFont(head, font->open_info.sub_font_index); + const std::uint32_t openFlag = lockWord & OPEN_BIT; + const std::uint32_t countBits = lockWord & COUNT_MASK; + if (openFlag != 0 && countBits != 0 && match) { + const std::uint32_t refcount = match->refcount; + const std::uint32_t decremented = lockWord - 1; + lockWord = decremented; + if (refcount == 1) { + FontObj* next = match->next; + FontObj* prev = match->prev; + library->sys_driver->close(match, 0); + if (!prev) *EntryObjectSlot(entry, modeLow) = next; + } else if (refcount != 0) { + match->refcount = refcount - 1; + } + if (countBits == 1) lockWord = ~openFlag & decremented; + } + ReleaseFontCtxEntryLock(entry, modeLow, lockWord); +} + +Font::FontState* Font::TryGetState(FontHandle handle) { + if (!handle) return nullptr; + std::lock_guard lock(stateMutex); + const auto it = fontStates.find(handle); + return it == fontStates.end() ? nullptr : it->second.get(); +} + +Font::FontState& Font::ResetState(FontHandle handle) { + std::lock_guard lock(stateMutex); + auto& slot = fontStates[handle]; + slot = std::make_unique(); + return *slot; +} + +void Font::RemoveState(FontHandle handle) { + if (!handle) return; + std::lock_guard lock(stateMutex); + fontStates.erase(handle); +} + +void Font::LoadStateFace(FontState& state, std::vector bytes, std::uint32_t subFontIndex) { + if (state.face) { + FT_Done_Face(state.face); + state.face = nullptr; + } + state.faceData = std::move(bytes); + const FT_Library library = HostFreeTypeLibrary(); + if (!library || state.faceData.empty()) return; + FT_Face face = nullptr; + if (FT_New_Memory_Face(library, state.faceData.data(), static_cast(state.faceData.size()), static_cast(subFontIndex), &face) != 0) return; + FT_Select_Charmap(face, FT_ENCODING_UNICODE); + state.face = face; +} + +void Font::TrackGeneratedGlyph(FontGlyph glyph) { + std::lock_guard lock(glyphMutex); + generatedGlyphs.insert(glyph); +} + +bool Font::ForgetGeneratedGlyph(FontGlyph glyph) { + std::lock_guard lock(glyphMutex); + return generatedGlyphs.erase(glyph) > 0; +} + +Font::GeneratedGlyph* Font::TryGetGeneratedGlyph(FontGlyph glyph) { + if (!glyph || glyph->magic != GLYPH_MAGIC) return nullptr; + std::lock_guard lock(glyphMutex); + if (!generatedGlyphs.contains(glyph)) return nullptr; + return reinterpret_cast(glyph); +} + +void Font::PopulateGlyphMetricVariants(GeneratedGlyph& glyph) { + if (glyph.metricsInitialized) return; + glyph.metricsHorizontal.width = glyph.metrics.width; + glyph.metricsHorizontal.height = glyph.metrics.height; + glyph.metricsHorizontal.horizontal.bearing_x = glyph.metrics.Horizontal.bearingX; + glyph.metricsHorizontal.horizontal.bearing_y = glyph.metrics.Horizontal.bearingY; + glyph.metricsHorizontal.horizontal.advance = glyph.metrics.Horizontal.advance; + glyph.metricsHorizontalX.width = glyph.metrics.width; + glyph.metricsHorizontalX.horizontal.bearing_x = glyph.metrics.Horizontal.bearingX; + glyph.metricsHorizontalX.horizontal.advance = glyph.metrics.Horizontal.advance; + glyph.metricsHorizontalAdvance.horizontal.advance = glyph.metrics.Horizontal.advance; + glyph.metricsInitialized = true; +} + +void Font::BuildBoundingOutline(GeneratedGlyph& glyph) { + const float left = glyph.metrics.Horizontal.bearingX; + const float top = glyph.metrics.Horizontal.bearingY; + const float right = left + glyph.metrics.width; + const float bottom = top - glyph.metrics.height; + glyph.outlinePoints = {{left, top}, {right, top}, {right, bottom}, {left, bottom}}; + glyph.outlineTags.assign(glyph.outlinePoints.size(), 1); + glyph.outlineContours = {static_cast(glyph.outlinePoints.size() - 1)}; + glyph.outline.points_ptr = glyph.outlinePoints.data(); + glyph.outline.tags_ptr = glyph.outlineTags.data(); + glyph.outline.contour_end_idx = glyph.outlineContours.data(); + glyph.outline.points_cnt = static_cast(glyph.outlinePoints.size()); + glyph.outline.contours_cnt = static_cast(glyph.outlineContours.size()); + glyph.outlineInitialized = true; +} + +bool Font::ValidStyleFrame(const FontStyleFrame* frame) { + return frame && frame->magic == STYLE_FRAME_MAGIC; +} + +int Font::StyleStateSetScalePixel(StyleStateBlock* style, float w, float h) { + if (!style) return 0; + if (style->scale_w == w && style->scale_h == h && style->scale_unit == 0) return 0; + style->scale_unit = 0; + style->scale_w = w; + style->scale_h = h; + return 1; +} + +int Font::StyleStateSetScalePoint(StyleStateBlock* style, float w, float h) { + if (!style) return 0; + if (style->scale_w == w && style->scale_h == h && style->scale_unit == 1) return 0; + style->scale_unit = 1; + style->scale_w = w; + style->scale_h = h; + return 1; +} + +int Font::StyleStateGetScalePixel(const StyleStateBlock* style, float* w, float* h) { + if (!style || (!w && !h)) return SCE_FONT_ERROR_INVALID_PARAMETER; + const bool points = style->scale_unit != 0; + if (w) { + float value = style->scale_w; + if (points && style->dpi_x != 0) value *= static_cast(style->dpi_x) / POINTS_PER_INCH; + *w = value; + } + if (h) { + float value = style->scale_h; + if (points && style->dpi_y != 0) value *= static_cast(style->dpi_y) / POINTS_PER_INCH; + *h = value; + } + return SCE_FONT_OK; +} + +int Font::StyleStateGetScalePoint(const StyleStateBlock* style, float* w, float* h) { + if (!style || (!w && !h)) return SCE_FONT_ERROR_INVALID_PARAMETER; + const bool pixels = style->scale_unit == 0; + if (w) { + float value = style->scale_w; + if (pixels && style->dpi_x != 0) value *= POINTS_PER_INCH / static_cast(style->dpi_x); + *w = value; + } + if (h) { + float value = style->scale_h; + if (pixels && style->dpi_y != 0) value *= POINTS_PER_INCH / static_cast(style->dpi_y); + *h = value; + } + return SCE_FONT_OK; +} + +int Font::StyleStateSetDpi(StyleStateBlock* style, std::uint32_t hDpi, std::uint32_t vDpi) { + if (!style) return 0; + if (hDpi == 0) hDpi = 0x48; + if (vDpi == 0) vDpi = 0x48; + if (style->dpi_x == hDpi && style->dpi_y == vDpi) return 0; + style->dpi_x = hDpi; + style->dpi_y = vDpi; + return 1; +} + +int Font::StyleStateSetSlantRatio(StyleStateBlock* style, float slantRatio) { + if (!style || style->slant_ratio == slantRatio) return 0; + style->slant_ratio = std::clamp(slantRatio, -1.0f, 1.0f); + return 1; +} + +int Font::StyleStateGetSlantRatio(const StyleStateBlock* style, float* slantRatio) { + if (!style || !slantRatio) return SCE_FONT_ERROR_INVALID_PARAMETER; + *slantRatio = style->slant_ratio; + return SCE_FONT_OK; +} + +int Font::StyleStateSetWeightScale(StyleStateBlock* style, float weightXScale, float weightYScale) { + if (!style) return 0; + const float deltaX = std::clamp(weightXScale - 1.0f, -0.04f, 0.04f); + const float deltaY = std::clamp(weightYScale - 1.0f, -0.04f, 0.04f); + bool changed = false; + if (style->effect_weight_x != deltaX) { + style->effect_weight_x = deltaX; + changed = true; + } + if (style->effect_weight_y != deltaY) { + style->effect_weight_y = deltaY; + changed = true; + } + return changed ? 1 : 0; +} + +int Font::StyleStateGetWeightScale(const StyleStateBlock* style, float* weightXScale, float* weightYScale, std::uint32_t* mode) { + if (!weightXScale && !weightYScale && !mode) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (!style) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (weightXScale) *weightXScale = style->effect_weight_x + 1.0f; + if (weightYScale) *weightYScale = style->effect_weight_y + 1.0f; + if (mode) *mode = 0; + return SCE_FONT_OK; +} + +std::uint8_t Font::CachedStyleCacheFlags(const CachedStyle& style) { + return static_cast(style.cache_flags_and_direction & 0xFFu); +} + +void Font::CachedStyleSetCacheFlags(CachedStyle& style, std::uint8_t flags) { + style.cache_flags_and_direction = (style.cache_flags_and_direction & 0xFFFFFF00u) | flags; +} + +void Font::CachedStyleSetDirectionWord(CachedStyle& style, std::uint16_t word) { + style.cache_flags_and_direction = (style.cache_flags_and_direction & 0x0000FFFFu) | (static_cast(word) << 16); +} + +float Font::CachedStyleGetScalar(const CachedStyle& style) { + float value; + std::memcpy(&value, &style.cached_scalar_bits, sizeof(value)); + return value; +} + +void Font::CachedStyleSetScalar(CachedStyle& style, float value) { + std::memcpy(&style.cached_scalar_bits, &value, sizeof(value)); +} + +void Font::ClearRenderOutputs(FontGlyphMetrics* metrics, FontRenderOutput* result) { + if (metrics) *metrics = {}; + if (result) *result = {}; +} diff --git a/core/libs/prx/libSceFont/src/Style.cpp b/core/libs/prx/libSceFont/src/Style.cpp new file mode 100644 index 000000000..42c4d9194 --- /dev/null +++ b/core/libs/prx/libSceFont/src/Style.cpp @@ -0,0 +1,303 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "prx/libSceFont/include/FontInternal.hpp" + +namespace { + +using namespace Font; + +template +int UpdateFontStyle(FontHandle fontHandle, Setter&& setter) { + auto* font = GetNativeFont(fontHandle); + std::uint32_t fontLock = 0; + if (!font || font->magic != HANDLE_MAGIC || !AcquireFontLock(font, fontLock)) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + if (setter(font) != 0) font->cached_style.layout_cache_state = 0; + ReleaseFontLock(font, fontLock); + return SCE_FONT_OK; +} + +template +int UpdateRenderStyle(FontHandle fontHandle, Setter&& setter) { + auto* font = GetNativeFont(fontHandle); + std::uint32_t cachedLock = 0; + if (!font || font->magic != HANDLE_MAGIC || !AcquireCachedStyleLock(font, cachedLock)) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + int rc = SCE_FONT_ERROR_NOT_BOUND_RENDERER; + if (font->renderer) rc = setter(font); + ReleaseCachedStyleLock(font, cachedLock); + return rc; +} + +template +int ReadRenderStyle(FontHandle fontHandle, bool checkMagic, Getter&& getter) { + auto* font = GetNativeFont(fontHandle); + std::uint32_t cachedLock = 0; + if (!font || (checkMagic && font->magic != HANDLE_MAGIC) || !AcquireCachedStyleLock(font, cachedLock)) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + const int rc = font->renderer ? getter(font) : SCE_FONT_ERROR_NOT_BOUND_RENDERER; + ReleaseCachedStyleLock(font, cachedLock); + return rc; +} + +template +int ReadFontStyle(FontHandle fontHandle, bool checkMagic, Getter&& getter) { + auto* font = GetNativeFont(fontHandle); + std::uint32_t fontLock = 0; + if (!font || (checkMagic && font->magic != HANDLE_MAGIC) || !AcquireFontLock(font, fontLock)) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + const int rc = getter(font); + ReleaseFontLock(font, fontLock); + return rc; +} + +void ResetScaleOutputs(float* w, float* h) { + if (w) *w = 0.0f; + if (h) *h = 0.0f; +} + +void ResetWeightOutputs(float* weightXScale, float* weightYScale, std::uint32_t* mode) { + if (weightXScale) *weightXScale = 1.0f; + if (weightYScale) *weightYScale = 1.0f; + if (mode) *mode = 0; +} + +float ClampWeightDelta(float scale) { + return std::clamp(scale - 1.0f, -0.04f, 0.04f); +} + +} + +#pragma GCC visibility push(default) + +extern "C" { + +int APS5_VABI sceFontSetScalePixel(FontHandle fontHandle, float w, float h) { + return UpdateFontStyle(fontHandle, [&](FontHandleNative* font) { + const int changed = StyleStateSetScalePixel(&font->style, w, h); + if (FontState* state = TryGetState(fontHandle)) { + state->scaleW = w; + state->scaleH = h; + } + return changed; + }); +} + +int APS5_VABI sceFontSetScalePoint(FontHandle fontHandle, float w, float h) { + return UpdateFontStyle(fontHandle, [&](FontHandleNative* font) { return StyleStateSetScalePoint(&font->style, w, h); }); +} + +int APS5_VABI sceFontSetResolutionDpi(FontHandle fontHandle, std::uint32_t hDpi, std::uint32_t vDpi) { + auto* font = GetNativeFont(fontHandle); + std::uint32_t fontLock = 0; + if (!font || !AcquireFontLock(font, fontLock)) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + if (StyleStateSetDpi(&font->style, hDpi, vDpi) != 0) font->cached_style.layout_cache_state = 0; + ReleaseFontLock(font, fontLock); + return SCE_FONT_OK; +} + +int APS5_VABI sceFontSetEffectSlant(FontHandle fontHandle, float slantRatio) { + return UpdateFontStyle(fontHandle, [&](FontHandleNative* font) { return StyleStateSetSlantRatio(&font->style, slantRatio); }); +} + +int APS5_VABI sceFontSetEffectWeight(FontHandle fontHandle, float weightXScale, float weightYScale, std::uint32_t mode) { + if (mode != 0) return SCE_FONT_ERROR_INVALID_PARAMETER; + return UpdateFontStyle(fontHandle, [&](FontHandleNative* font) { return StyleStateSetWeightScale(&font->style, weightXScale, weightYScale); }); +} + +int APS5_VABI sceFontGetScalePixel(FontHandle fontHandle, float* w, float* h) { + const int rc = ReadFontStyle(fontHandle, true, [&](FontHandleNative* font) { return StyleStateGetScalePixel(&font->style, w, h); }); + if (rc != SCE_FONT_OK) ResetScaleOutputs(w, h); + return rc; +} + +int APS5_VABI sceFontGetScalePoint(FontHandle fontHandle, float* w, float* h) { + const int rc = ReadFontStyle(fontHandle, false, [&](FontHandleNative* font) { return StyleStateGetScalePoint(&font->style, w, h); }); + if (rc != SCE_FONT_OK) ResetScaleOutputs(w, h); + return rc; +} + +int APS5_VABI sceFontGetEffectSlant(FontHandle fontHandle, float* slantRatio) { + const int rc = ReadFontStyle(fontHandle, true, [&](FontHandleNative* font) { return StyleStateGetSlantRatio(&font->style, slantRatio); }); + if (rc != SCE_FONT_OK && slantRatio) *slantRatio = 0.0f; + return rc; +} + +int APS5_VABI sceFontGetEffectWeight(FontHandle fontHandle, float* weightXScale, float* weightYScale, std::uint32_t* mode) { + const int rc = ReadFontStyle(fontHandle, true, [&](FontHandleNative* font) { return StyleStateGetWeightScale(&font->style, weightXScale, weightYScale, mode); }); + if (rc != SCE_FONT_OK) ResetWeightOutputs(weightXScale, weightYScale, mode); + return rc; +} + +int APS5_VABI sceFontSetupRenderScalePixel(FontHandle fontHandle, float w, float h) { + return UpdateRenderStyle(fontHandle, [&](FontHandleNative* font) { + if (StyleStateSetScalePixel(&font->cached_style.state, w, h) != 0) font->cached_style.cache_flags_and_direction &= 0xFFFF0000u; + return SCE_FONT_OK; + }); +} + +int APS5_VABI sceFontSetupRenderScalePoint(FontHandle fontHandle, float w, float h) { + return UpdateRenderStyle(fontHandle, [&](FontHandleNative* font) { + if (StyleStateSetScalePoint(&font->cached_style.state, w, h) != 0) font->cached_style.cache_flags_and_direction &= 0xFFFF0000u; + return SCE_FONT_OK; + }); +} + +int APS5_VABI sceFontSetupRenderEffectSlant(FontHandle fontHandle, float slantRatio) { + return UpdateRenderStyle(fontHandle, [&](FontHandleNative* font) { + if (StyleStateSetSlantRatio(&font->cached_style.state, slantRatio) != 0) font->cached_style.cache_flags_and_direction &= 0xFFFF0000u; + return SCE_FONT_OK; + }); +} + +int APS5_VABI sceFontSetupRenderEffectWeight(FontHandle fontHandle, float weightXScale, float weightYScale, std::uint32_t mode) { + return UpdateRenderStyle(fontHandle, [&](FontHandleNative* font) { + if (mode != 0) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (StyleStateSetWeightScale(&font->cached_style.state, weightXScale, weightYScale) != 0) font->cached_style.cache_flags_and_direction &= 0xFFFF0000u; + return SCE_FONT_OK; + }); +} + +int APS5_VABI sceFontGetRenderScalePixel(FontHandle fontHandle, float* w, float* h) { + const int rc = ReadRenderStyle(fontHandle, false, [&](FontHandleNative* font) { return StyleStateGetScalePixel(&font->cached_style.state, w, h); }); + if (rc != SCE_FONT_OK) ResetScaleOutputs(w, h); + return rc; +} + +int APS5_VABI sceFontGetRenderScalePoint(FontHandle fontHandle, float* w, float* h) { + const int rc = ReadRenderStyle(fontHandle, false, [&](FontHandleNative* font) { return StyleStateGetScalePoint(&font->cached_style.state, w, h); }); + if (rc != SCE_FONT_OK) ResetScaleOutputs(w, h); + return rc; +} + +int APS5_VABI sceFontGetRenderEffectSlant(FontHandle fontHandle, float* slantRatio) { + const int rc = ReadRenderStyle(fontHandle, true, [&](FontHandleNative* font) { return StyleStateGetSlantRatio(&font->cached_style.state, slantRatio); }); + if (rc != SCE_FONT_OK && slantRatio) *slantRatio = 0.0f; + return rc; +} + +int APS5_VABI sceFontGetRenderEffectWeight(FontHandle fontHandle, float* weightXScale, float* weightYScale, std::uint32_t* mode) { + const int rc = ReadRenderStyle(fontHandle, true, [&](FontHandleNative* font) { return StyleStateGetWeightScale(&font->cached_style.state, weightXScale, weightYScale, mode); }); + if (rc != SCE_FONT_OK) ResetWeightOutputs(weightXScale, weightYScale, mode); + return rc; +} + +int APS5_VABI sceFontStyleFrameInit(FontStyleFrame* styleFrame) { + if (!styleFrame) return SCE_FONT_ERROR_INVALID_PARAMETER; + std::memset(&styleFrame->scaleUnit, 0, 0x20); + std::memset(&styleFrame->layout_cache_state, 0, 0x20); + std::memset(&styleFrame->layout_cache_bytes[0x08], 0, 0x20); + styleFrame->magic = STYLE_FRAME_MAGIC; + styleFrame->flags1 = 0; + styleFrame->flags2 = 0; + styleFrame->hDpi = 0x48; + styleFrame->vDpi = 0x48; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontStyleFrameSetScalePixel(FontStyleFrame* styleFrame, float w, float h) { + if (!ValidStyleFrame(styleFrame)) return SCE_FONT_ERROR_INVALID_PARAMETER; + styleFrame->scaleUnit = 0; + styleFrame->scalePixelW = w; + styleFrame->scalePixelH = h; + styleFrame->flags1 |= STYLE_FRAME_FLAG_SCALE; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontStyleFrameSetScalePoint(FontStyleFrame* styleFrame, float w, float h) { + if (!ValidStyleFrame(styleFrame)) return SCE_FONT_ERROR_INVALID_PARAMETER; + styleFrame->scaleUnit = 1; + styleFrame->scalePixelW = w; + styleFrame->scalePixelH = h; + styleFrame->flags1 |= STYLE_FRAME_FLAG_SCALE; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontStyleFrameSetResolutionDpi(FontStyleFrame* styleFrame, std::uint32_t hDpi, std::uint32_t vDpi) { + if (!ValidStyleFrame(styleFrame)) return SCE_FONT_ERROR_INVALID_PARAMETER; + styleFrame->hDpi = hDpi == 0 ? 0x48 : hDpi; + styleFrame->vDpi = vDpi == 0 ? 0x48 : vDpi; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontStyleFrameSetEffectSlant(FontStyleFrame* styleFrame, float slantRatio) { + if (!ValidStyleFrame(styleFrame)) return SCE_FONT_ERROR_INVALID_PARAMETER; + styleFrame->slantRatio = std::clamp(slantRatio, -1.0f, 1.0f); + styleFrame->flags1 |= STYLE_FRAME_FLAG_SLANT; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontStyleFrameSetEffectWeight(FontStyleFrame* styleFrame, float weightXScale, float weightYScale, std::uint32_t mode) { + if (!ValidStyleFrame(styleFrame) || mode != 0) return SCE_FONT_ERROR_INVALID_PARAMETER; + styleFrame->effectWeightX = ClampWeightDelta(weightXScale); + styleFrame->effectWeightY = ClampWeightDelta(weightYScale); + styleFrame->flags1 |= STYLE_FRAME_FLAG_WEIGHT; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontStyleFrameUnsetScale(FontStyleFrame* styleFrame) { + if (!ValidStyleFrame(styleFrame)) return SCE_FONT_ERROR_INVALID_PARAMETER; + styleFrame->flags1 &= static_cast(~STYLE_FRAME_FLAG_SCALE); + return SCE_FONT_OK; +} + +int APS5_VABI sceFontStyleFrameUnsetEffectSlant(FontStyleFrame* styleFrame) { + if (!ValidStyleFrame(styleFrame)) return SCE_FONT_ERROR_INVALID_PARAMETER; + styleFrame->flags1 &= static_cast(~STYLE_FRAME_FLAG_SLANT); + return SCE_FONT_OK; +} + +int APS5_VABI sceFontStyleFrameUnsetEffectWeight(FontStyleFrame* styleFrame) { + if (!ValidStyleFrame(styleFrame)) return SCE_FONT_ERROR_INVALID_PARAMETER; + styleFrame->flags1 &= static_cast(~STYLE_FRAME_FLAG_WEIGHT); + return SCE_FONT_OK; +} + +int APS5_VABI sceFontStyleFrameGetScalePixel(const FontStyleFrame* styleFrame, float* w, float* h) { + if (!ValidStyleFrame(styleFrame)) return SCE_FONT_ERROR_INVALID_PARAMETER; + if ((styleFrame->flags1 & STYLE_FRAME_FLAG_SCALE) == 0) return SCE_FONT_ERROR_UNSET_PARAMETER; + if (!w && !h) return SCE_FONT_ERROR_INVALID_PARAMETER; + const bool points = styleFrame->scaleUnit != 0; + if (w) *w = points && styleFrame->hDpi != 0 ? styleFrame->scalePixelW * (static_cast(styleFrame->hDpi) / POINTS_PER_INCH) : styleFrame->scalePixelW; + if (h) *h = points && styleFrame->vDpi != 0 ? styleFrame->scalePixelH * (static_cast(styleFrame->vDpi) / POINTS_PER_INCH) : styleFrame->scalePixelH; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontStyleFrameGetScalePoint(const FontStyleFrame* styleFrame, float* w, float* h) { + if (!ValidStyleFrame(styleFrame)) return SCE_FONT_ERROR_INVALID_PARAMETER; + if ((styleFrame->flags1 & STYLE_FRAME_FLAG_SCALE) == 0) return SCE_FONT_ERROR_UNSET_PARAMETER; + if (!w && !h) return SCE_FONT_ERROR_INVALID_PARAMETER; + const bool pixels = styleFrame->scaleUnit == 0; + if (w) *w = pixels && styleFrame->hDpi != 0 ? styleFrame->scalePixelW * (POINTS_PER_INCH / static_cast(styleFrame->hDpi)) : styleFrame->scalePixelW; + if (h) *h = pixels && styleFrame->vDpi != 0 ? styleFrame->scalePixelH * (POINTS_PER_INCH / static_cast(styleFrame->vDpi)) : styleFrame->scalePixelH; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontStyleFrameGetResolutionDpi(const FontStyleFrame* styleFrame, std::uint32_t* hDpi, std::uint32_t* vDpi) { + if (!ValidStyleFrame(styleFrame) || (!hDpi && !vDpi)) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (hDpi) *hDpi = styleFrame->hDpi; + if (vDpi) *vDpi = styleFrame->vDpi; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontStyleFrameGetEffectSlant(const FontStyleFrame* styleFrame, float* slantRatio) { + if (!ValidStyleFrame(styleFrame)) return SCE_FONT_ERROR_INVALID_PARAMETER; + if ((styleFrame->flags1 & STYLE_FRAME_FLAG_SLANT) == 0) return SCE_FONT_ERROR_UNSET_PARAMETER; + if (!slantRatio) return SCE_FONT_ERROR_INVALID_PARAMETER; + *slantRatio = styleFrame->slantRatio; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontStyleFrameGetEffectWeight(const FontStyleFrame* styleFrame, float* weightXScale, float* weightYScale, std::uint32_t* mode) { + if (!ValidStyleFrame(styleFrame)) return SCE_FONT_ERROR_INVALID_PARAMETER; + if ((styleFrame->flags1 & STYLE_FRAME_FLAG_WEIGHT) == 0) return SCE_FONT_ERROR_UNSET_PARAMETER; + if (!weightXScale && !weightYScale && !mode) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (weightXScale) *weightXScale = styleFrame->effectWeightX + 1.0f; + if (weightYScale) *weightYScale = styleFrame->effectWeightY + 1.0f; + if (mode) *mode = 0; + return SCE_FONT_OK; +} + +} + +#pragma GCC visibility pop diff --git a/core/libs/prx/libSceFont/src/Text.cpp b/core/libs/prx/libSceFont/src/Text.cpp new file mode 100644 index 000000000..5a43ff63f --- /dev/null +++ b/core/libs/prx/libSceFont/src/Text.cpp @@ -0,0 +1,633 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include + +#include "prx/libSceFont/include/FontInternal.hpp" + +namespace { + +using namespace Font; + +constexpr std::uint16_t TEXT_SOURCE_MAGIC = 0x0F04; +constexpr std::uint16_t FONT_STRING_MAGIC = 0x0F05; +constexpr std::uint16_t FONT_WRITING_MAGIC = 0x0F06; +constexpr std::uint16_t CREATE_STRING_DETAIL_ID = 0x0FD4; +constexpr std::uint32_t WRITING_FORM_HORIZONTAL = 0x10; +constexpr std::uint32_t WRITING_FORM_HORIZONTAL_LTR = 0x12; +constexpr std::int32_t PARSER_RESULT_ERROR = -1; +constexpr std::int32_t PARSER_RESULT_TERMINATE = 0; +constexpr std::int32_t PARSER_RESULT_FONT_CODE = 1; +constexpr std::size_t MAX_PARSED_CHARACTERS = 4096; +constexpr std::int32_t WRITING_MASK_NONE = 0; +constexpr std::int32_t WRITING_MASK_FORMAT_CHARACTERS = 1; +constexpr std::uint64_t CHARACTER_FLAG_FORMAT = 1ull << 42; + +struct CharacterStorage { + std::atomic refCount{1}; + std::vector characters; +}; + +struct FontStringData { + std::uint16_t magic = FONT_STRING_MAGIC; + std::uint8_t flags = 0; + std::uint8_t reserved0 = 0; + std::uint32_t writingForm = WRITING_FORM_HORIZONTAL; + const FontMemory* memory = nullptr; + FontTextSource* source = nullptr; + FontHandle defaultFont = nullptr; + std::uint32_t terminateCode = 0; + void* terminateOrder = nullptr; + std::uint32_t textCount = 0; + std::uint32_t renderCount = 0; + CharacterStorage* storage = nullptr; +}; + +struct FontWritingData { + std::uint16_t magic = FONT_WRITING_MAGIC; + std::uint16_t reserved0 = 0; + std::uint32_t writingForm = WRITING_FORM_HORIZONTAL; + FontString string = nullptr; + CharacterStorage* storage = nullptr; + FontHandle defaultFont = nullptr; + std::size_t nextIndex = 0; + std::size_t currentIndex = 0; + float penX = 0.0f; + float penY = 0.0f; + bool hasAggregateMetrics = false; + bool hasCurrentStep = false; + bool maskFormatCharacters = true; + bool ownsStorage = false; + FontWritingStep step{}; + FontWritingMetrics metrics{}; + FontGlyphMetrics rawGlyphMetrics{}; + bool hasRawGlyphMetrics = false; +}; + +static_assert(sizeof(FontWritingData) <= sizeof(FontWriting)); + +FontStringData* GetStringData(FontString fontString) { + auto* data = reinterpret_cast(fontString); + return data && data->magic == FONT_STRING_MAGIC ? data : nullptr; +} + +FontWritingData* GetWritingData(FontWriting* fontWriting) { + if (!fontWriting) return nullptr; + auto* data = reinterpret_cast(fontWriting->systemUse); + return data->magic == FONT_WRITING_MAGIC ? data : nullptr; +} + +void ReleaseStorage(CharacterStorage*& storage) { + if (!storage) return; + if (storage->refCount.fetch_sub(1, std::memory_order_acq_rel) == 1) delete storage; + storage = nullptr; +} + +void ReleaseWritingStorage(FontWritingData& writing) { + if (!writing.ownsStorage) return; + ReleaseStorage(writing.storage); + writing.ownsStorage = false; +} + +std::uint16_t TextSourceMagic(const FontTextSource* source) { + return static_cast(source->systemUse0 & 0xFFFFu); +} + +bool IsWhitespaceCode(std::uint32_t code) { + switch (code) { + case 0x0009: case 0x000A: case 0x000B: case 0x000C: case 0x000D: case 0x0020: case 0x0085: + case 0x00A0: case 0x1680: case 0x2028: case 0x2029: case 0x202F: case 0x205F: case 0x3000: + return true; + default: + return code >= 0x2000 && code <= 0x200A; + } +} + +bool IsFormatCode(std::uint32_t code) { + switch (code) { + case 0x00AD: case 0x061C: case 0x200B: case 0x200C: case 0x200D: case 0x200E: case 0x200F: + case 0x2060: case 0x2061: case 0x2062: case 0x2063: case 0x2064: case 0x2066: case 0x2067: + case 0x2068: case 0x2069: case 0xFE0E: case 0xFE0F: case 0xFEFF: + return true; + default: + return false; + } +} + +FontTextCharacter MakeCharacter(FontHandle font, std::uint32_t code, void* textOrder) { + FontTextCharacter character{}; + character.textOrder = textOrder; + character.font = font; + character.characterCode = code; + character.clusterSpan = 1; + character.flags = static_cast(IsWhitespaceCode(code) ? 0x0E : 0) << 8; + if (IsFormatCode(code)) character.flags |= CHARACTER_FLAG_FORMAT; + return character; +} + +void LinkCharacters(std::vector& characters) { + for (std::size_t i = 0; i < characters.size(); ++i) { + characters[i].prev = i == 0 ? nullptr : &characters[i - 1]; + characters[i].next = i + 1 < characters.size() ? &characters[i + 1] : nullptr; + } +} + +FontTextCharacter* TextCharacters(FontStringData* data) { + return data && data->storage && !data->storage->characters.empty() ? data->storage->characters.data() : nullptr; +} + +std::uint32_t LevelOf(const FontTextCharacter* character) { + return static_cast((character->flags >> 24) & 0xFFu); +} + +void FillWritingMetricsFromGlyph(FontWritingData& writing, const FontGlyphMetrics& glyphMetrics) { + writing.rawGlyphMetrics = glyphMetrics; + writing.hasRawGlyphMetrics = true; + writing.step.advanceX = glyphMetrics.Horizontal.advance; + writing.step.advanceY = glyphMetrics.Vertical.advance; + writing.step.GlyphMetrics = glyphMetrics; + writing.step.Positioning.x = writing.step.x; + writing.step.Positioning.y = writing.step.y; + writing.step.GlyphMetrics.width = writing.step.x; + writing.step.GlyphMetrics.height = writing.step.y; +} + +bool PopulateWritingStep(FontWritingData& writing, std::size_t index) { + if (!writing.storage || index >= writing.storage->characters.size()) return false; + const FontTextCharacter& character = writing.storage->characters[index]; + const FontHandle font = character.font ? character.font : writing.defaultFont; + writing.step.x = writing.penX; + writing.step.y = writing.penY; + writing.step.advanceX = 0.0f; + writing.step.advanceY = 0.0f; + writing.step.font = font; + writing.step.Profile = {}; + writing.step.Profile.characterCount = 1; + writing.step.glyphCode = character.characterCode; + writing.step.Positioning.x = 0.0f; + writing.step.Positioning.y = 0.0f; + writing.step.GlyphMetrics = {}; + writing.rawGlyphMetrics = {}; + writing.hasRawGlyphMetrics = false; + if ((character.flags & CHARACTER_FLAG_FORMAT) != 0 && writing.maskFormatCharacters) writing.step.Profile.invisibleGlyph = 1; + if (font && character.characterCode != 0) { + FontGlyphMetrics glyphMetrics{}; + int rc = GetCharGlyphMetrics(font, character.characterCode, &glyphMetrics, true); + if (rc != SCE_FONT_OK) rc = GetCharGlyphMetrics(font, character.characterCode, &glyphMetrics, false); + if (rc == SCE_FONT_OK) FillWritingMetricsFromGlyph(writing, glyphMetrics); + } + writing.penX += writing.step.advanceX; + writing.penY += writing.step.advanceY; + return true; +} + +void AccumulateWritingMetrics(FontWritingData& writing) { + const FontWritingMetrics previous = writing.metrics; + const FontGlyphMetrics& glyphMetrics = writing.hasRawGlyphMetrics ? writing.rawGlyphMetrics : writing.step.GlyphMetrics; + const float left = writing.step.x + glyphMetrics.Horizontal.bearingX; + const float right = left + glyphMetrics.width; + const float top = writing.step.y - glyphMetrics.Horizontal.bearingY; + const float bottom = top + glyphMetrics.height; + writing.metrics.advanceX = writing.step.x + writing.step.advanceX; + writing.metrics.advanceY = writing.step.y + writing.step.advanceY; + if (!writing.hasAggregateMetrics) { + writing.metrics.Extent = {top, bottom, left, right}; + writing.hasAggregateMetrics = true; + return; + } + writing.metrics.Extent.left = std::min(previous.Extent.left, left); + writing.metrics.Extent.right = std::max(previous.Extent.right, right); + writing.metrics.Extent.top = std::min(previous.Extent.top, top); + writing.metrics.Extent.bottom = std::max(previous.Extent.bottom, bottom); +} + +} + +#pragma GCC visibility push(default) + +extern "C" { + +int APS5_VABI sceFontTextSourceInit(FontTextSource* textSource, const void* textAddress, std::uint32_t textSizeByte, FontTextParseFunction textParser, void* textObject) { + if (!textSource) return SCE_FONT_ERROR_INVALID_PARAMETER; + std::memset(textSource, 0, sizeof(*textSource)); + if (!textParser) return SCE_FONT_ERROR_INVALID_PARAMETER; + const void* end = textSizeByte != 0 ? static_cast(textAddress) + textSizeByte : nullptr; + textSource->systemUse0 = (static_cast(WRITING_FORM_HORIZONTAL) << 32) | TEXT_SOURCE_MAGIC; + textSource->start = textAddress; + textSource->end = end; + textSource->current = textAddress; + textSource->textParser = textParser; + textSource->textObject = textObject; + textSource->defaultFont = nullptr; + textSource->systemUse[0] = const_cast(textAddress); + textSource->systemUse[1] = const_cast(end); + textSource->systemUse[2] = reinterpret_cast(textParser); + textSource->systemUse[3] = textObject; + textSource->systemUse[4] = nullptr; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontTextSourceRewind(FontTextSource* textSource) { + if (!textSource || TextSourceMagic(textSource) != TEXT_SOURCE_MAGIC) return SCE_FONT_ERROR_INVALID_PARAMETER; + textSource->start = textSource->systemUse[0]; + textSource->end = textSource->systemUse[1]; + textSource->current = textSource->systemUse[0]; + textSource->textParser = reinterpret_cast(textSource->systemUse[2]); + textSource->textObject = textSource->systemUse[3]; + textSource->defaultFont = static_cast(textSource->systemUse[4]); + return SCE_FONT_OK; +} + +int APS5_VABI sceFontTextSourceSetDefaultFont(FontTextSource* textSource, FontHandle defaultFont) { + if (!textSource || TextSourceMagic(textSource) != TEXT_SOURCE_MAGIC) return SCE_FONT_ERROR_INVALID_PARAMETER; + textSource->defaultFont = defaultFont; + textSource->systemUse[4] = defaultFont; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontTextSourceSetWritingForm(FontTextSource* textSource, std::int32_t writingForm) { + if (!textSource || TextSourceMagic(textSource) != TEXT_SOURCE_MAGIC) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (writingForm < static_cast(WRITING_FORM_HORIZONTAL) || writingForm > static_cast(WRITING_FORM_HORIZONTAL_LTR)) return SCE_FONT_ERROR_INVALID_PARAMETER; + textSource->systemUse0 = (textSource->systemUse0 & 0xFFFFFFFFull) | (static_cast(writingForm) << 32); + return SCE_FONT_OK; +} + +int APS5_VABI sceFontCreateString(const FontMemory* fontMemory, FontTextSource* textSource, const FontCreateStringDetail* detail, FontString* pFontString) { + if (!fontMemory || !textSource || !pFontString || !textSource->textParser) { + if (pFontString) *pFontString = nullptr; + return SCE_FONT_ERROR_INVALID_PARAMETER; + } + *pFontString = nullptr; + if (TextSourceMagic(textSource) != TEXT_SOURCE_MAGIC) return SCE_FONT_ERROR_INVALID_TEXT_SOURCE; + if (fontMemory->mem_kind != MEMORY_MAGIC || !fontMemory->iface || !fontMemory->iface->alloc || !fontMemory->iface->dealloc) return SCE_FONT_ERROR_INVALID_MEMORY; + if (detail) { + constexpr std::uint32_t validOrders = (1u << 0) | (1u << 1) | (1u << 2) | (1u << 31); + if (detail->detailId != CREATE_STRING_DETAIL_ID || detail->detailType > 1 || (detail->detections & ~0x07u) != 0 || (detail->ordersOption & ~validOrders) != 0) return SCE_FONT_ERROR_INVALID_PARAMETER; + } + void* raw = fontMemory->iface->alloc(fontMemory->mspace_handle, sizeof(FontStringData)); + if (!raw) return SCE_FONT_ERROR_ALLOCATION_FAILED; + auto* data = new (raw) FontStringData{}; + data->memory = fontMemory; + data->source = textSource; + data->defaultFont = textSource->defaultFont; + data->storage = new CharacterStorage{}; + if (!data->defaultFont && detail) data->defaultFont = detail->defaultFont; + data->writingForm = static_cast(textSource->systemUse0 >> 32); + std::int32_t state = SCE_FONT_ERROR_INVALID_TEXT_SOURCE; + void* textOrder = nullptr; + for (std::size_t parsed = 0; parsed < MAX_PARSED_CHARACTERS; ++parsed) { + FontTextParseResult result{}; + state = textSource->textParser(textSource, &textOrder, &result); + if (state == PARSER_RESULT_FONT_CODE) { + const FontHandle font = result.FontCode.font ? result.FontCode.font : data->defaultFont; + if (!font) { + state = SCE_FONT_ERROR_INVALID_TEXT_SOURCE; + break; + } + data->storage->characters.push_back(MakeCharacter(font, result.FontCode.code, textOrder)); + continue; + } + if (state == PARSER_RESULT_TERMINATE) { + data->terminateOrder = textOrder; + data->terminateCode = result.Terminate.terminateCode; + state = SCE_FONT_OK; + } else if (state == PARSER_RESULT_ERROR) { + state = result.Error.errorCode < 0 ? result.Error.errorCode : SCE_FONT_ERROR_INVALID_TEXT_SOURCE; + } else { + state = SCE_FONT_ERROR_INVALID_TEXT_SOURCE; + } + break; + } + if (state == PARSER_RESULT_FONT_CODE) state = SCE_FONT_ERROR_INVALID_TEXT_SOURCE; + if (state != SCE_FONT_OK) { + ReleaseStorage(data->storage); + data->~FontStringData(); + fontMemory->iface->dealloc(fontMemory->mspace_handle, raw); + return state; + } + LinkCharacters(data->storage->characters); + data->textCount = static_cast(data->storage->characters.size()); + data->renderCount = data->textCount; + *pFontString = reinterpret_cast(data); + return SCE_FONT_OK; +} + +int APS5_VABI sceFontDestroyString(FontString* pFontString) { + if (!pFontString) return SCE_FONT_ERROR_INVALID_PARAMETER; + auto* data = GetStringData(*pFontString); + if (!data) { + *pFontString = nullptr; + return SCE_FONT_ERROR_INVALID_STRING; + } + const FontMemory* memory = data->memory; + ReleaseStorage(data->storage); + data->~FontStringData(); + memory->iface->dealloc(memory->mspace_handle, data); + *pFontString = nullptr; + return SCE_FONT_OK; +} + +std::uint32_t APS5_VABI sceFontStringGetTerminateCode(FontString fontString) { + const auto* data = GetStringData(fontString); + return data ? data->terminateCode : 0; +} + +void* APS5_VABI sceFontStringGetTerminateOrder(FontString fontString) { + const auto* data = GetStringData(fontString); + return data ? data->terminateOrder : nullptr; +} + +int APS5_VABI sceFontStringGetWritingForm(FontString fontString) { + const auto* data = GetStringData(fontString); + return data ? static_cast(data->writingForm) : 0; +} + +FontTextCharacter* APS5_VABI sceFontStringRefersTextCharacters(FontString fontString, std::uint32_t* characterCount) { + auto* data = GetStringData(fontString); + if (!data || data->textCount == 0) { + if (characterCount) *characterCount = 0; + return nullptr; + } + if (characterCount) *characterCount = data->textCount; + return TextCharacters(data); +} + +const FontTextCharacter* APS5_VABI sceFontStringRefersRenderCharacters(FontString fontString, FontTextCharacter* startCharacter, FontTextCharacter* lastCharacter, std::uint32_t* characterCount) { + auto* data = GetStringData(fontString); + if (!data || !characterCount || data->renderCount == 0) { + if (characterCount) *characterCount = 0; + return nullptr; + } + FontTextCharacter* characters = TextCharacters(data); + if (!characters) { + *characterCount = 0; + return nullptr; + } + const FontTextCharacter* begin = characters; + const FontTextCharacter* end = characters + data->renderCount; + if (!startCharacter) startCharacter = characters; + if (startCharacter < begin || startCharacter >= end || (lastCharacter && (lastCharacter < begin || lastCharacter >= end))) { + *characterCount = 0; + return nullptr; + } + FontTextCharacter* probe = startCharacter; + if (startCharacter->clusterKind > 1) { + do { + startCharacter = probe; + probe = startCharacter->prev; + if (!probe) break; + } while (probe->synthetic < 0); + } + auto startFlags = static_cast(startCharacter->flags); + const std::uint32_t paragraphLevel = (data->writingForm >> 8) & 1u; + if ((startFlags & 0xFF00u) == 0x100u && startCharacter->prev) { + if (startCharacter->prev->clusterKind == 1) startCharacter = startCharacter->prev; + startFlags = static_cast(startCharacter->flags); + } + std::uint32_t startLevel = startFlags >> 24; + std::uint32_t lastLevel = startLevel; + std::uint32_t minLevel = startLevel; + std::uint32_t count = startCharacter->clusterIndex == 0 ? 1u : 0u; + probe = startCharacter; + while (probe != lastCharacter) { + probe = probe->next; + if (!probe) { + if (lastCharacter) { + *characterCount = 0; + return nullptr; + } + break; + } + if (probe->clusterIndex == 0) { + lastLevel = LevelOf(probe); + if (lastLevel < minLevel && ((lastLevel ^ paragraphLevel) & 1u) == 0) minLevel = lastLevel; + ++count; + } + } + if (startLevel != lastLevel || startLevel != minLevel) { + probe = startCharacter->prev; + while (probe) { + if (probe->clusterIndex == 0) { + if (LevelOf(probe) <= minLevel) break; + ++count; + } + startCharacter = probe; + probe = probe->prev; + } + if (lastCharacter) { + probe = lastCharacter->next; + while (probe) { + if (probe->clusterIndex == 0) { + if (LevelOf(probe) <= minLevel) break; + ++count; + lastCharacter = probe; + } + probe = probe->next; + } + } + } + if (lastCharacter) { + probe = lastCharacter->next; + std::uint32_t clusterTrim = 0; + while (probe) { + std::uint32_t nextTrim = clusterTrim; + if (probe->clusterIndex == 0) { + if (probe->synthetic == 0) break; + nextTrim = 0; + FontTextCharacter* nextLast = probe; + if (probe->synthetic < 1) { + if (probe->clusterKind == 1) break; + nextTrim = clusterTrim + 1; + nextLast = lastCharacter; + } + lastCharacter = nextLast; + ++count; + } + probe = probe->next; + count -= nextTrim; + clusterTrim = nextTrim; + } + } + startLevel = LevelOf(startCharacter); + if (startLevel != paragraphLevel) { + probe = startCharacter; + if (!lastCharacter || LevelOf(lastCharacter) != startLevel) { + do { + probe = probe->next; + if (!probe) { + if (lastCharacter) startCharacter = nullptr; + break; + } + FontTextCharacter* best = startCharacter; + std::uint32_t bestLevel = startLevel; + if (probe->clusterIndex == 0) { + const std::uint32_t probeLevel = LevelOf(probe); + if (probeLevel <= paragraphLevel) break; + if ((startLevel & 1u) == paragraphLevel && probeLevel < startLevel && (probeLevel & 1u) != paragraphLevel) { + best = probe; + bestLevel = probeLevel; + } + } + startLevel = bestLevel; + startCharacter = best; + } while (probe != lastCharacter); + } else if ((LevelOf(startCharacter) & 1u) != paragraphLevel) { + startCharacter = lastCharacter; + } + } + if (!startCharacter) count = 0; + *characterCount = count; + return startCharacter; +} + +int APS5_VABI sceFontCharacterGetBidiLevel(const FontTextCharacter* textCharacter, int* bidiLevel) { + if (!textCharacter || !bidiLevel) return SCE_FONT_ERROR_INVALID_PARAMETER; + *bidiLevel = static_cast(LevelOf(textCharacter)); + return SCE_FONT_OK; +} + +int APS5_VABI sceFontCharacterGetTextFontCode(const FontTextCharacter* textCharacter, FontHandle* pFontHandle, std::uint32_t* textCode) { + if (!textCharacter) { + if (pFontHandle) *pFontHandle = nullptr; + if (textCode) *textCode = 0; + return SCE_FONT_ERROR_INVALID_PARAMETER; + } + if (!pFontHandle || !textCode) return SCE_FONT_ERROR_INVALID_PARAMETER; + *pFontHandle = textCharacter->font; + *textCode = textCharacter->characterCode; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontCharacterGetTextOrder(const FontTextCharacter* textCharacter, void** pTextOrder) { + if (!pTextOrder) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (!textCharacter) { + *pTextOrder = nullptr; + return SCE_FONT_ERROR_INVALID_PARAMETER; + } + *pTextOrder = textCharacter->textOrder; + return SCE_FONT_OK; +} + +std::uint32_t APS5_VABI sceFontCharacterLooksFormatCharacters(const FontTextCharacter* textCharacter) { + if (!textCharacter) return 0; + return (textCharacter->flags & CHARACTER_FLAG_FORMAT) != 0 ? textCharacter->characterCode : 0; +} + +std::uint32_t APS5_VABI sceFontCharacterLooksWhiteSpace(const FontTextCharacter* textCharacter) { + if (!textCharacter) return 0; + return ((textCharacter->flags >> 8) & 0xFFu) == 0x0E ? textCharacter->characterCode : 0; +} + +FontTextCharacter* APS5_VABI sceFontCharacterRefersTextBack(const FontTextCharacter* textCharacter) { + if (!textCharacter) return nullptr; + for (FontTextCharacter* current = textCharacter->prev; current; current = current->prev) { + if (current->synthetic == 0 && current->clusterIndex == 0) return current; + } + return nullptr; +} + +FontTextCharacter* APS5_VABI sceFontCharacterRefersTextNext(const FontTextCharacter* textCharacter) { + if (!textCharacter) return nullptr; + for (FontTextCharacter* current = textCharacter->next; current; current = current->next) { + if (current->synthetic == 0 && current->clusterIndex == 0) return current; + } + return nullptr; +} + +int APS5_VABI sceFontWritingInit(FontWriting* fontWriting, FontString fontString, const FontTextCharacter* fontCharacter) { + if (!fontWriting || !fontString || !fontCharacter) return SCE_FONT_ERROR_INVALID_PARAMETER; + auto* data = GetStringData(fontString); + if (!data) return SCE_FONT_ERROR_INVALID_STRING; + if (auto* previous = GetWritingData(fontWriting)) ReleaseWritingStorage(*previous); + std::memset(fontWriting, 0, sizeof(*fontWriting)); + auto* writing = new (fontWriting->systemUse) FontWritingData{}; + writing->string = fontString; + writing->storage = data->storage; + writing->defaultFont = data->defaultFont; + if (writing->storage) writing->storage->refCount.fetch_add(1, std::memory_order_relaxed); + writing->ownsStorage = true; + writing->writingForm = data->writingForm; + const FontTextCharacter* characters = TextCharacters(data); + if (!characters || fontCharacter < characters || fontCharacter >= characters + data->storage->characters.size()) { + ReleaseWritingStorage(*writing); + return SCE_FONT_ERROR_INVALID_PARAMETER; + } + writing->nextIndex = static_cast(fontCharacter - characters); + writing->currentIndex = writing->nextIndex; + writing->hasCurrentStep = false; + if (!PopulateWritingStep(*writing, writing->nextIndex)) { + ReleaseWritingStorage(*writing); + return SCE_FONT_ERROR_INVALID_PARAMETER; + } + writing->penX = 0.0f; + writing->penY = 0.0f; + writing->hasAggregateMetrics = false; + writing->metrics = {}; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontWritingGetRenderMetrics(FontWriting* fontWriting, FontWritingMetrics* writingMetrics) { + if (!fontWriting || !writingMetrics) { + if (writingMetrics) *writingMetrics = {}; + return SCE_FONT_ERROR_INVALID_PARAMETER; + } + const auto* writing = GetWritingData(fontWriting); + if (!writing) { + *writingMetrics = {}; + return SCE_FONT_ERROR_INVALID_WRITING; + } + *writingMetrics = writing->metrics; + return SCE_FONT_OK; +} + +const FontWritingStep* APS5_VABI sceFontWritingRefersRenderStep(FontWriting* fontWriting) { + auto* writing = GetWritingData(fontWriting); + if (!writing || !writing->storage || writing->nextIndex >= writing->storage->characters.size()) return nullptr; + if (!PopulateWritingStep(*writing, writing->nextIndex)) return nullptr; + writing->currentIndex = writing->nextIndex; + writing->hasCurrentStep = true; + AccumulateWritingMetrics(*writing); + ++writing->nextIndex; + return &writing->step; +} + +FontTextCharacter* APS5_VABI sceFontWritingRefersRenderStepCharacter(FontWriting* fontWriting, FontWritingLetterStep* letterStep) { + if (letterStep) std::memset(letterStep, 0, sizeof(*letterStep)); + auto* writing = GetWritingData(fontWriting); + if (!writing || !writing->hasCurrentStep || !writing->storage || writing->currentIndex >= writing->storage->characters.size()) return nullptr; + if (letterStep) { + letterStep->x = writing->step.x; + letterStep->y = writing->step.y; + letterStep->advanceX = writing->step.advanceX; + letterStep->advanceY = writing->step.advanceY; + letterStep->Components.textsCount = 1; + letterStep->Components.textsIndex = static_cast(writing->currentIndex); + letterStep->Components.glyphsCount = 1; + letterStep->Components.glyphsIndex = static_cast(writing->currentIndex); + letterStep->Components.characterTextCount = static_cast(std::max(writing->step.Profile.characterCount, 1)); + } + return &writing->storage->characters[writing->currentIndex]; +} + +int APS5_VABI sceFontWritingSetMaskInvisible(FontWriting* fontWriting, std::int32_t mask) { + auto* writing = GetWritingData(fontWriting); + if (!writing) return SCE_FONT_ERROR_INVALID_WRITING; + if (mask == WRITING_MASK_FORMAT_CHARACTERS) { + writing->maskFormatCharacters = false; + return SCE_FONT_OK; + } + if (mask == WRITING_MASK_NONE) { + writing->maskFormatCharacters = true; + return SCE_FONT_OK; + } + return SCE_FONT_ERROR_INVALID_PARAMETER; +} + +} + +#pragma GCC visibility pop diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp new file mode 100644 index 000000000..de6b6e3d0 --- /dev/null +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -0,0 +1,477 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "prx/libc/include/General.hpp" + +#pragma GCC visibility push(default) + +extern "C" { + +int APS5_VABI sceFontCharacterGetSyllableStringState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontCharactersRefersTextCodes() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontCreateGraphicsDevice() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontCreateGraphicsService() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontCreateGraphicsServiceWithEdition() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontCreateWords() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontCreateWritingLine() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontDefineAttribute() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontDestroyGraphicsDevice() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontDestroyGraphicsService() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontDestroyWords() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontDestroyWritingLine() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontDettachDeviceCacheBuffer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGetAttribute() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGetCharGlyphCode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGetFontGlyphsCount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGetFontGlyphsOutlineProfile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGetFontMetrics() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGetFontResolution() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGetFontStyleInformation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGetGlyphExpandBufferState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGetPixelResolution() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGetRenderScaledKerning() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGetResolutionDpi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGetScriptLanguage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGetTypographicDesign() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGlyphDefineAttribute() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGlyphGetAttribute() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGlyphRenderImage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGlyphRenderImageHorizontal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGlyphRenderImageVertical() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsBeginFrame() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsDrawingCancel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsDrawingFinish() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsEndFrame() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsExchangeResource() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsFillMethodInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsFillPlotInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsFillPlotSetLayout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsFillPlotSetMapping() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsFillRatesInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsFillRatesSetFillEffect() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsFillRatesSetLayout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsFillRatesSetMapping() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsGetDeviceUsage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsRegionInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsRegionInitCircular() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsRegionInitRoundish() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsRelease() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsRenderResource() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsSetFramePolicy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsSetupClipping() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsSetupColorRates() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsSetupFillMethod() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsSetupFillRates() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsSetupGlyphFill() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsSetupGlyphFillPlot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsSetupHandleDefault() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsSetupLocation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsSetupPositioning() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsSetupRotation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsSetupScaling() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsSetupShapeFill() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsSetupShapeFillPlot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsStructureCanvas() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsStructureCanvasSequence() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsStructureDesign() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsStructureDesignResource() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsStructureSurfaceTexture() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsUpdateClipping() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsUpdateColorRates() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsUpdateFillMethod() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsUpdateFillRates() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsUpdateGlyphFill() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsUpdateGlyphFillPlot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsUpdateLocation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsUpdatePositioning() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsUpdateRotation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsUpdateScaling() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsUpdateShapeFill() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsUpdateShapeFillPlot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontSetScriptLanguage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontSetTypographicDesign() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontSupportGlyphs() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontTextCodesStepBack() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontTextCodesStepNext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontWordsFindWordCharacters() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontWritingLineClear() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontWritingLineGetOrderingSpace() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontWritingLineGetRenderMetrics() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontWritingLineRefersRenderStep() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontWritingLineWritesOrder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} + +#pragma GCC visibility pop diff --git a/core/libs/prx/libSceFontFt/CMakeLists.txt b/core/libs/prx/libSceFontFt/CMakeLists.txt new file mode 100644 index 000000000..c1fa26487 --- /dev/null +++ b/core/libs/prx/libSceFontFt/CMakeLists.txt @@ -0,0 +1,15 @@ +find_package(Freetype REQUIRED) + +add_library(libSceFontFt SHARED EXCLUDE_FROM_ALL + ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Driver.cpp +) + +target_include_directories(libSceFontFt PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceFontFt PRIVATE Freetype::Freetype libc) +set_target_properties(libSceFontFt PROPERTIES + CXX_EXTENSIONS OFF + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON +) +configure_windows_unwind(libSceFontFt) diff --git a/core/libs/prx/libSceFontFt/Export.cpp b/core/libs/prx/libSceFontFt/Export.cpp index a15c312e4..d34a3ca55 100644 --- a/core/libs/prx/libSceFontFt/Export.cpp +++ b/core/libs/prx/libSceFontFt/Export.cpp @@ -1,20 +1,116 @@ -#include -#include -#include "SceTypes.hpp" +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "prx/libSceFontFt/include/FontFtDriver.hpp" #include "prx/libc/include/General.hpp" +#pragma GCC visibility push(default) + extern "C" { -void* APS5_VABI sceFontSelectLibraryFt(int value) { - (void)value; - NotImplemented_nid_no_patch(__func__); - return nullptr; +const Font::SysDriver* APS5_VABI sceFontSelectLibraryFt(int value) { + return value == 0 ? FontFt::DriverTable() : nullptr; } -void* APS5_VABI sceFontSelectRendererFt(int value) { - (void)value; - NotImplemented_nid_no_patch(__func__); - return nullptr; +const Font::RendererSelection* APS5_VABI sceFontSelectRendererFt(int value) { + return value == 0 ? FontFt::RendererTable() : nullptr; +} + +int APS5_VABI sceFontFtInitAliases() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontFtSetAliasFont() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontFtSetAliasPath() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontFtSupportBdf() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontFtSupportCid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontFtSupportFontFormats() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontFtSupportOpenType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontFtSupportOpenTypeOtf() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontFtSupportOpenTypeTtf() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontFtSupportPcf() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontFtSupportPfr() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontFtSupportSystemFonts() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontFtSupportTrueType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontFtSupportTrueTypeGx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontFtSupportType1() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontFtSupportType42() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontFtSupportWinFonts() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontFtTermAliases() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontSelectGlyphsFt() { + NotImplemented_nid_no_patch(__func__); + return 0; } } + +#pragma GCC visibility pop diff --git a/core/libs/prx/libSceFontFt/include/FontFtDriver.hpp b/core/libs/prx/libSceFontFt/include/FontFtDriver.hpp new file mode 100644 index 000000000..87e7e9293 --- /dev/null +++ b/core/libs/prx/libSceFontFt/include/FontFtDriver.hpp @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef CORE_LIBS_PRX_LIBSCEFONTFT_INCLUDE_FONTFTDRIVER_HPP +#define CORE_LIBS_PRX_LIBSCEFONTFT_INCLUDE_FONTFTDRIVER_HPP + +#include "prx/libSceFont/include/FontDriver.hpp" + +namespace FontFt { + +const Font::SysDriver* DriverTable(); +const Font::RendererSelection* RendererTable(); + +} + +#endif diff --git a/core/libs/prx/libSceFontFt/src/Driver.cpp b/core/libs/prx/libSceFontFt/src/Driver.cpp new file mode 100644 index 000000000..71c1ff144 --- /dev/null +++ b/core/libs/prx/libSceFontFt/src/Driver.cpp @@ -0,0 +1,711 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include + +#include +#include FT_FREETYPE_H +#include FT_MODULE_H +#include FT_SYSTEM_H +#include FT_TRUETYPE_TABLES_H + +#include "prx/libSceFont/include/FontFreeType.hpp" +#include "prx/libSceFontFt/include/FontFtDriver.hpp" + +namespace { + +using namespace Font; + +struct FtLibraryContext { + void* allocCtx; + const FontMemoryInterface* iface; + FT_Memory memory; + FT_Library library; +}; + +void* FtAlloc(FT_Memory memory, long size) { + if (!memory || size <= 0) return nullptr; + auto* ctx = static_cast(memory->user); + if (!ctx || !ctx->iface || !ctx->iface->alloc) return nullptr; + return ctx->iface->alloc(ctx->allocCtx, static_cast(size)); +} + +void FtFree(FT_Memory memory, void* block) { + if (!memory || !block) return; + auto* ctx = static_cast(memory->user); + if (!ctx || !ctx->iface || !ctx->iface->dealloc) return; + ctx->iface->dealloc(ctx->allocCtx, block); +} + +void* FtRealloc(FT_Memory memory, long currentSize, long newSize, void* block) { + if (!memory) return nullptr; + auto* ctx = static_cast(memory->user); + if (!ctx || !ctx->iface) return nullptr; + if (ctx->iface->realloc_fn) return ctx->iface->realloc_fn(ctx->allocCtx, block, static_cast(newSize)); + if (newSize <= 0) { + FtFree(memory, block); + return nullptr; + } + void* result = FtAlloc(memory, newSize); + if (!result) return nullptr; + if (block && currentSize > 0) std::memcpy(result, block, static_cast(std::min(currentSize, newSize))); + FtFree(memory, block); + return result; +} + +void WriteU16(std::uint8_t* out, std::size_t offset, std::uint16_t value) { + std::memcpy(out + offset, &value, sizeof(value)); +} + +void WriteU32(std::uint8_t* out, std::size_t offset, std::uint32_t value) { + std::memcpy(out + offset, &value, sizeof(value)); +} + +void WriteS32(std::uint8_t* out, std::size_t offset, std::int32_t value) { + std::memcpy(out + offset, &value, sizeof(value)); +} + +void WritePointer(std::uint8_t* out, std::size_t offset, const void* value) { + std::memcpy(out + offset, &value, sizeof(value)); +} + +float FromF26Dot6(std::int64_t value) { + return static_cast(value) * ONE_OVER_64; +} + +FT_Face FaceOf(const FontObj* obj) { + return static_cast(obj->ft_face); +} + +std::uint32_t APS5_VABI LibraryGetPixelResolution() { + return 0x40; +} + +int APS5_VABI LibraryInit(const FontMemory* memory, FontLibNative* library) { + if (!memory || !library) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (!memory->iface || !memory->iface->alloc || !memory->iface->dealloc) return SCE_FONT_ERROR_INVALID_MEMORY; + const auto allocFn = memory->iface->alloc; + const auto freeFn = memory->iface->dealloc; + void* allocCtx = memory->mspace_handle; + auto* ctx = static_cast(allocFn(allocCtx, sizeof(FtLibraryContext))); + if (!ctx) return SCE_FONT_ERROR_ALLOCATION_FAILED; + std::memset(ctx, 0, sizeof(FtLibraryContext)); + ctx->allocCtx = allocCtx; + ctx->iface = memory->iface; + auto* ftMemory = static_cast(allocFn(allocCtx, sizeof(FT_MemoryRec_))); + if (!ftMemory) { + freeFn(allocCtx, ctx); + return SCE_FONT_ERROR_ALLOCATION_FAILED; + } + std::memset(ftMemory, 0, sizeof(*ftMemory)); + ftMemory->user = ctx; + ftMemory->alloc = &FtAlloc; + ftMemory->free = &FtFree; + ftMemory->realloc = &FtRealloc; + ctx->memory = ftMemory; + FT_Library ftLibrary = nullptr; + if (FT_New_Library(ftMemory, &ftLibrary) != 0 || !ftLibrary) { + freeFn(allocCtx, ftMemory); + freeFn(allocCtx, ctx); + return SCE_FONT_ERROR_ALLOCATION_FAILED; + } + FT_Add_Default_Modules(ftLibrary); + ctx->library = ftLibrary; + library->fontset_registry = ctx; + library->flags = 0x60000000; + return SCE_FONT_OK; +} + +int APS5_VABI LibraryTerm(FontLibNative* library) { + if (!library) return SCE_FONT_ERROR_INVALID_PARAMETER; + const auto freeFn = library->iface ? library->iface->dealloc : nullptr; + if (!freeFn) return SCE_FONT_ERROR_INVALID_PARAMETER; + auto* ctx = static_cast(library->fontset_registry); + if (!ctx) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (ctx->library) { + FT_Done_Library(ctx->library); + ctx->library = nullptr; + } + if (ctx->memory) { + freeFn(library->alloc_ctx, ctx->memory); + ctx->memory = nullptr; + } + freeFn(library->alloc_ctx, ctx); + library->fontset_registry = nullptr; + return SCE_FONT_OK; +} + +int APS5_VABI LibrarySupport(FontLibNative* library, std::uint32_t formats) { + (void)formats; + if (!library || !library->fontset_registry) return SCE_FONT_ERROR_INVALID_PARAMETER; + return SCE_FONT_OK; +} + +int APS5_VABI LibraryOpenFont(FontLibNative* library, std::uint32_t mode, const void* fontAddress, std::uint32_t fontSize, std::uint32_t subFontIndex, std::uint32_t uniqueWord, FontObj** inoutFontObj) { + (void)uniqueWord; + if (!library || !fontAddress || !inoutFontObj) return SCE_FONT_ERROR_INVALID_PARAMETER; + const auto allocFn = library->iface ? library->iface->alloc : nullptr; + const auto freeFn = library->iface ? library->iface->dealloc : nullptr; + if (!allocFn || !freeFn) return SCE_FONT_ERROR_INVALID_PARAMETER; + const bool fromMemory = mode == 1; + if (fromMemory) { + if (fontSize == 0) return SCE_FONT_ERROR_INVALID_PARAMETER; + } else if (mode == 5 || mode == 6 || mode == 7) { + if (static_cast(fontAddress)[0] == '\0') return SCE_FONT_ERROR_INVALID_PARAMETER; + } else { + return SCE_FONT_ERROR_INVALID_PARAMETER; + } + auto* ctx = static_cast(library->fontset_registry); + if (!ctx || !ctx->library) return SCE_FONT_ERROR_INVALID_LIBRARY; + FT_Face face = nullptr; + FT_Error error; + if (fromMemory) { + error = FT_New_Memory_Face(ctx->library, static_cast(fontAddress), static_cast(fontSize), static_cast(subFontIndex), &face); + } else { + error = FT_New_Face(ctx->library, static_cast(fontAddress), static_cast(subFontIndex), &face); + } + if (error != 0 || !face) { + if (fromMemory || error == FT_Err_Unknown_File_Format) return SCE_FONT_ERROR_NO_SUPPORT_FORMAT; + return SCE_FONT_ERROR_FS_OPEN_FAILED; + } + FT_Select_Charmap(face, FT_ENCODING_UNICODE); + auto* obj = static_cast(allocFn(library->alloc_ctx, sizeof(FontObj))); + if (!obj) { + FT_Done_Face(face); + return SCE_FONT_ERROR_ALLOCATION_FAILED; + } + std::memset(obj, 0, sizeof(FontObj)); + obj->refcount = 1; + obj->sub_font_index = subFontIndex; + obj->prev = nullptr; + obj->next = *inoutFontObj; + if (obj->next) obj->next->prev = obj; + obj->ft_face = face; + obj->ft_ctx_0x58 = &ctx->library; + *inoutFontObj = obj; + return SCE_FONT_OK; +} + +int APS5_VABI LibraryCloseFont(FontObj* obj, std::uint32_t flags) { + (void)flags; + if (!obj) return SCE_FONT_ERROR_FATAL; + if (obj->refcount > 1) { + --obj->refcount; + return SCE_FONT_OK; + } + FT_Face face = FaceOf(obj); + FtLibraryContext* ctx = face && face->memory ? static_cast(face->memory->user) : nullptr; + const auto freeFn = ctx && ctx->iface ? ctx->iface->dealloc : nullptr; + if (face) { + FT_Done_Face(face); + obj->ft_face = nullptr; + } + if (!freeFn) return SCE_FONT_ERROR_FATAL; + FontObj* next = obj->next; + FontObj* prev = obj->prev; + if (prev) prev->next = next; + if (next) next->prev = prev; + freeFn(ctx->allocCtx, obj); + return SCE_FONT_OK; +} + +int APS5_VABI LibraryGetFaceScale(FontObj* obj, std::uint16_t* outUnitsPerEm, float* outScale) { + if (!obj || !outUnitsPerEm || !outScale) return SCE_FONT_ERROR_FATAL; + const FT_Face face = FaceOf(obj); + if (!face) return SCE_FONT_ERROR_FATAL; + const std::uint16_t units = face->units_per_EM; + *outUnitsPerEm = units; + *outScale = static_cast(units) * ONE_OVER_64; + return SCE_FONT_OK; +} + +int APS5_VABI LibraryGetFaceMetric(FontObj* obj, std::uint32_t metricId, std::uint16_t* outMetric) { + if (!obj || !outMetric) return SCE_FONT_ERROR_FATAL; + const FT_Face face = FaceOf(obj); + if (!face) return SCE_FONT_ERROR_FATAL; + const std::uint16_t units = face->units_per_EM; + if (metricId == 0x0E00) { + *outMetric = units; + return SCE_FONT_OK; + } + if (metricId == 0xEA00) { + if (const auto* os2 = static_cast(FT_Get_Sfnt_Table(face, FT_SFNT_OS2))) { + *outMetric = static_cast(os2->sTypoAscender); + return SCE_FONT_OK; + } + *outMetric = units; + return SCE_FONT_ERROR_NO_SUPPORT_FUNCTION; + } + *outMetric = 0; + return SCE_FONT_OK; +} + +int APS5_VABI LibraryGetGlyphIndex(FontObj* obj, std::uint32_t codepoint, std::uint32_t* outGlyphIndex) { + if (!obj || !outGlyphIndex) return SCE_FONT_ERROR_FATAL; + const FT_Face face = FaceOf(obj); + if (!face) { + *outGlyphIndex = 0; + return SCE_FONT_ERROR_FATAL; + } + const std::uint32_t glyphIndex = ResolveGlyphIndexWithFallback(face, codepoint); + *outGlyphIndex = glyphIndex; + return glyphIndex ? SCE_FONT_OK : SCE_FONT_ERROR_NO_SUPPORT_GLYPH; +} + +int APS5_VABI LibrarySetCharSizeWithDpi(FontObj* obj, std::uint32_t dpiX, std::uint32_t dpiY, float scaleX, float scaleY, float* outScaleX, float* outScaleY) { + if (!obj || !outScaleX || !outScaleY) return SCE_FONT_ERROR_FATAL; + const FT_Face face = FaceOf(obj); + if (!face) return SCE_FONT_ERROR_FATAL; + const auto charW = static_cast(static_cast(scaleX * 64.0f)); + const auto charH = static_cast(static_cast(scaleY * 64.0f)); + if (SetCharSizeCompat(face, charW, charH, dpiX, dpiY) != 0) return SCE_FONT_ERROR_FATAL; + if (!face->size) return SCE_FONT_ERROR_FATAL; + const std::uint16_t units = face->units_per_EM; + *outScaleX = FixedMulUnitsToF26Dot6(static_cast(face->size->metrics.x_scale), units); + *outScaleY = FixedMulUnitsToF26Dot6(static_cast(face->size->metrics.y_scale), units); + return SCE_FONT_OK; +} + +int APS5_VABI LibrarySetCharSizeDefaultDpi(FontObj* obj, float scaleX, float scaleY, float* outScaleX, float* outScaleY) { + if (!obj || !outScaleX || !outScaleY) return SCE_FONT_ERROR_FATAL; + return LibrarySetCharSizeWithDpi(obj, 0x48, 0x48, scaleX, scaleY, outScaleX, outScaleY); +} + +int APS5_VABI LibraryComputeLayout(FontObj* obj, const StyleStateBlock* style, std::uint8_t* outWords) { + if (!obj || !style || !outWords) return SCE_FONT_ERROR_FATAL; + const FT_Face face = FaceOf(obj); + if (!face || !face->size) return SCE_FONT_ERROR_FATAL; + const float effectWidth = style->effect_weight_x; + const float effectHeight = style->effect_weight_y; + const float slant = style->slant_ratio; + const auto yScale = static_cast(face->size->metrics.y_scale); + const auto xScale = static_cast(face->size->metrics.x_scale); + const std::int64_t yShift = obj->shift_cache_y; + const std::int64_t xShift = obj->shift_cache_x; + const auto unitsPerEm = static_cast(static_cast(face->units_per_EM)); + std::int32_t yMinPx = RoundMulFixed(static_cast(face->bbox.yMin) + yShift, yScale); + std::int32_t yMaxPx = RoundMulFixed(static_cast(face->bbox.yMax) + yShift, yScale); + std::int32_t halfEffectWidthPx = 0; + std::int32_t leftAdjustPx = 0; + if (effectWidth != 0.0f) { + const std::int32_t unitsScaledX = TruncFixed(unitsPerEm * xScale); + halfEffectWidthPx = TruncateFloatToInt(effectWidth * static_cast(unitsScaledX)) / 2; + leftAdjustPx = -halfEffectWidthPx; + } + float outEffectHeight = 0.0f; + if (effectHeight != 0.0f) { + const std::int32_t unitsScaledY = TruncFixed(unitsPerEm * yScale); + const std::int32_t halfEffectHeightPx = TruncateFloatToInt(effectHeight * static_cast(unitsScaledY)) / 2; + outEffectHeight = static_cast(halfEffectHeightPx) * ONE_OVER_64; + yMinPx -= halfEffectHeightPx; + yMaxPx += halfEffectHeightPx; + } + if (slant != 0.0f) { + const auto shear = static_cast(TruncateFloatToInt(slant * 65536.0f)); + leftAdjustPx += RoundMulFixed(yMinPx, shear); + halfEffectWidthPx += RoundMulFixed(yMaxPx, shear); + } + StoreFloat(outWords, HORIZONTAL_EFFECT_HEIGHT, outEffectHeight); + StoreFloat(outWords, HORIZONTAL_LEFT_ADJUST, static_cast(leftAdjustPx) * ONE_OVER_64); + StoreFloat(outWords, HORIZONTAL_HALF_EFFECT_WIDTH, static_cast(halfEffectWidthPx) * ONE_OVER_64); + const std::int32_t xMinPx = RoundMulFixed(static_cast(face->bbox.xMin) + xShift, xScale); + const std::int32_t xMaxPx = RoundMulFixed(static_cast(face->bbox.xMax) + xShift, xScale); + StoreFloat(outWords, HORIZONTAL_LINE_ADVANCE, static_cast(yMaxPx - yMinPx) * ONE_OVER_64); + StoreFloat(outWords, HORIZONTAL_BASELINE, static_cast(yMaxPx) * ONE_OVER_64); + StoreFloat(outWords, HORIZONTAL_X_BOUND_LO, static_cast(xMinPx + leftAdjustPx) * ONE_OVER_64); + StoreFloat(outWords, HORIZONTAL_X_BOUND_HI, static_cast(xMaxPx + halfEffectWidthPx) * ONE_OVER_64); + const std::int32_t maxAdvanceWidthPx = RoundMulFixed(static_cast(face->max_advance_width) + xShift, xScale); + StoreFloat(outWords, HORIZONTAL_MAX_ADVANCE_WIDTH, static_cast(maxAdvanceWidthPx) * ONE_OVER_64); + float caretRiseAdjust = 0.0f; + if (const auto* hhea = static_cast(FT_Get_Sfnt_Table(face, FT_SFNT_HHEA))) { + const std::int64_t caretRiseUnits = xShift + static_cast(hhea->caret_Slope_Rise); + const std::int32_t caretRisePx = TruncFixed(caretRiseUnits * xScale); + caretRiseAdjust = static_cast(caretRisePx - halfEffectWidthPx) * ONE_OVER_64; + } + StoreFloat(outWords, HORIZONTAL_CARET_RISE_ADJUST, caretRiseAdjust); + return SCE_FONT_OK; +} + +int APS5_VABI LibraryComputeLayoutAlt(FontObj* obj, const StyleStateBlock* style, std::uint8_t* outWords) { + if (!obj || !style || !outWords) return SCE_FONT_ERROR_FATAL; + const FT_Face face = FaceOf(obj); + if (!face || !face->size) return SCE_FONT_ERROR_FATAL; + const float effectWidth = style->effect_weight_x; + const float effectHeight = style->effect_weight_y; + const float slant = style->slant_ratio; + const auto xScale = static_cast(face->size->metrics.x_scale); + const auto yScale = static_cast(face->size->metrics.y_scale); + const auto* vhea = static_cast(FT_Get_Sfnt_Table(face, FT_SFNT_VHEA)); + const std::int64_t yShift = obj->shift_cache_y; + const std::int64_t xShift = obj->shift_cache_x; + const auto units = static_cast(face->units_per_EM); + std::int64_t yAscender = 0; + std::int64_t yDescender = 0; + if (vhea) { + yAscender = static_cast(vhea->Ascender) + yShift; + yDescender = static_cast(vhea->Descender) + yShift; + } + const std::int32_t scaledLeft = RoundMulFixed(static_cast(face->bbox.xMin) + xShift, xScale); + const std::int32_t scaledRight = RoundMulFixed(static_cast(face->bbox.xMax) + xShift, xScale); + std::int32_t xMinScaled = scaledLeft; + std::int32_t xAbsMax = -xMinScaled; + if (xAbsMax < scaledRight) { + xAbsMax = scaledRight; + xMinScaled = -scaledRight; + } + float outEffectWidth = 0.0f; + if (effectWidth != 0.0f) { + const auto unitsScaled = static_cast(TruncMulUnits(xScale, units)); + const std::int32_t effect = TruncateFloatToInt(effectWidth * static_cast(unitsScaled)) / 2; + outEffectWidth = static_cast(effect) * ONE_OVER_64; + } + if (effectHeight != 0.0f) { + const auto unitsScaled = static_cast(TruncMulUnits(yScale, units)); + const std::int32_t delta = TruncateFloatToInt(effectHeight * static_cast(unitsScaled)) / 2; + yAscender -= delta; + yDescender += delta; + } + float outSlantA = 0.0f; + float outSlantB = 0.0f; + if (slant != 0.0f) { + const auto shear = static_cast(TruncateFloatToInt(slant * 65536.0f)); + const std::int64_t signAdjustMin = yAscender < 1 ? 0x10000LL : 0LL; + std::int64_t roundedMin = signAdjustMin + (-yAscender) * shear - 0x8000LL; + if (roundedMin < 0) roundedMin = signAdjustMin + 0x7FFFLL + (-yAscender) * shear; + const std::int64_t signAdjustMax = yDescender >= 0 ? 0x10000LL : 0LL; + std::int64_t roundedMax = signAdjustMax + yDescender * shear - 0x8000LL; + if (roundedMax < 0) roundedMax = signAdjustMax + 0x7FFFLL + yDescender * shear; + const auto roundedMaxBits = static_cast(roundedMax); + const auto roundedMinBits = static_cast(roundedMin); + const std::uint64_t roundedMaxHigh = roundedMaxBits >> 16; + const bool maxRoundBitClear = ((roundedMaxBits >> 15) & 1u) == 0; + std::uint64_t selectedHigh = roundedMaxHigh; + auto selectedLow = static_cast(roundedMinBits >> 16); + if (maxRoundBitClear && roundedMaxHigh != 0) { + selectedHigh = roundedMinBits >> 16; + selectedLow = static_cast(roundedMaxBits >> 16); + } + outSlantA = static_cast(selectedLow) * ONE_OVER_64; + outSlantB = static_cast(static_cast(selectedHigh)) * ONE_OVER_64; + } + StoreFloat(outWords, VERTICAL_COLUMN_ADVANCE, static_cast(xAbsMax - xMinScaled) * ONE_OVER_64); + StoreFloat(outWords, VERTICAL_BASELINE_OFFSET_X, static_cast(-xAbsMax) * ONE_OVER_64); + StoreFloat(outWords, VERTICAL_METRICS_0X08, static_cast(RoundMulFixed(yAscender, xScale)) * ONE_OVER_64); + StoreFloat(outWords, VERTICAL_METRICS_0X0C, static_cast(RoundMulFixed(yDescender, xScale)) * ONE_OVER_64); + const std::int32_t advanceHeight = RoundMulFixed(static_cast(face->max_advance_height) + yShift, yScale); + StoreFloat(outWords, VERTICAL_ADVANCE_HEIGHT, static_cast(advanceHeight) * ONE_OVER_64); + StoreFloat(outWords, VERTICAL_DECORATION_SPAN, outEffectWidth); + StoreFloat(outWords, VERTICAL_DECORATION_0X08, outSlantB); + StoreFloat(outWords, VERTICAL_DECORATION_0X0C, outSlantA); + return SCE_FONT_OK; +} + +void StoreSeedVector(FontObj* obj) { + const auto seedLow = static_cast(static_cast(obj->layout_seed_pair & 0xFFFFFFFFu)); + const auto seedHigh = static_cast(static_cast(obj->layout_seed_pair >> 32)); + obj->layout_seed_vec[0] = static_cast(static_cast(seedLow)); + obj->layout_seed_vec[1] = static_cast(static_cast(seedHigh)); +} + +void WriteOutlineSizes(FT_GlyphSlot slot, std::uint64_t* outWords) { + const auto contours = static_cast(slot->outline.n_contours); + const auto points = static_cast(slot->outline.n_points); + const std::uint32_t size4 = points * 0x10u + 0x68u + ((points + 0x0Fu + contours * 2u) & 0xFFFFFFF0u); + const std::uint32_t size8 = ((points + 0x2Bu + contours * 2u) & 0xFFFFFFFCu) + 0x10u + points * 4u; + auto* out = reinterpret_cast(outWords); + WriteU16(out, 0, static_cast(contours)); + WriteU16(out, 2, static_cast(points)); + WriteU32(out, 4, size4); + WriteU32(out, 8, size8); +} + +int APS5_VABI LibraryLoadGlyphCached(FontObj* obj, std::uint32_t glyphIndex, std::int32_t mode, std::uint64_t* outWords) { + if (outWords) { + outWords[0] = 0; + outWords[1] = 0; + } + if (!obj || !outWords) return SCE_FONT_ERROR_FATAL; + const FT_Face face = FaceOf(obj); + if (!face || !face->glyph) return SCE_FONT_ERROR_FATAL; + const auto unitsPerEm = static_cast(face->units_per_EM); + const bool cachedMatch = obj->cached_glyph_index_0x64 == static_cast(glyphIndex) && obj->cached_units_x_0x68 == unitsPerEm && obj->cached_units_y_0x70 == obj->cached_units_x_0x68 && mode == 0; + if (cachedMatch) { + obj->shift_cache_x = obj->shift_units_x; + obj->shift_cache_y = obj->shift_units_y; + StoreSeedVector(obj); + WriteOutlineSizes(face->glyph, outWords); + return SCE_FONT_OK; + } + const FT_Int32 loadFlags = (mode == 0 ? FT_LOAD_NO_SCALE : 0) | FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP | FT_LOAD_VERTICAL_LAYOUT; + const FT_Error error = FT_Load_Glyph(face, glyphIndex, loadFlags); + if (error != 0) { + obj->cached_glyph_index_0x64 = 0; + obj->cached_units_x_0x68 = 0; + obj->cached_units_y_0x70 = 0; + obj->layout_seed_vec = {}; + obj->layout_scale_vec = {}; + return error == FT_Err_Out_Of_Memory ? SCE_FONT_ERROR_ALLOCATION_FAILED : SCE_FONT_ERROR_NO_SUPPORT_GLYPH; + } + if (!face->glyph) return SCE_FONT_ERROR_FATAL; + obj->cached_glyph_index_0x64 = static_cast(glyphIndex); + if (mode == 0) { + obj->cached_units_x_0x68 = unitsPerEm; + obj->cached_units_y_0x70 = unitsPerEm; + obj->shift_cache_x = obj->shift_units_x; + obj->shift_cache_y = obj->shift_units_y; + StoreSeedVector(obj); + obj->layout_scale_vec[0] = 0x10000; + obj->layout_scale_vec[1] = 0x10000; + } else { + if (!face->size) return SCE_FONT_ERROR_FATAL; + const auto xScale = static_cast(face->size->metrics.x_scale); + const auto yScale = static_cast(face->size->metrics.y_scale); + obj->cached_units_x_0x68 = static_cast(TruncMulUnits(xScale, unitsPerEm)); + obj->cached_units_y_0x70 = static_cast(TruncMulUnits(yScale, unitsPerEm)); + obj->shift_cache_x = RoundMulFixed(obj->shift_units_x, xScale); + obj->shift_cache_y = RoundMulFixed(obj->shift_units_y, yScale); + const auto seedLow = static_cast(static_cast(obj->layout_seed_pair & 0xFFFFFFFFu)); + const auto seedHigh = static_cast(static_cast(obj->layout_seed_pair >> 32)); + obj->layout_seed_vec[0] = static_cast(static_cast(RoundMulFixed(seedLow, xScale))); + obj->layout_seed_vec[1] = static_cast(static_cast(RoundMulFixed(seedHigh, yScale))); + obj->layout_scale_vec[0] = static_cast(xScale); + obj->layout_scale_vec[1] = static_cast(yScale); + } + WriteOutlineSizes(face->glyph, outWords); + return SCE_FONT_OK; +} + +std::int64_t RoundMulFixed64(std::int64_t value, std::int64_t fixed16) { + return static_cast(RoundMulFixed(value, fixed16)); +} + +int APS5_VABI LibraryGetGlyphMetrics(FontObj* obj, std::uint32_t* optParam2, std::uint8_t mode, std::uint8_t* outParams, FontGlyphMetrics* outMetrics) { + if (!obj || !outParams || !outMetrics) return SCE_FONT_ERROR_FATAL; + const FT_Face face = FaceOf(obj); + if (!face || !face->glyph || !face->size) return SCE_FONT_ERROR_FATAL; + const FT_GlyphSlot slot = face->glyph; + std::memset(outParams, 0, 0x48); + const void* outlineBlob = &slot->outline; + outParams[0] = 0xF2; + WritePointer(outParams, 0x08, slot); + if (optParam2) { + outlineBlob = optParam2; + outParams[0] = 0xF0; + WritePointer(outParams, 0x08, optParam2); + } + const auto faceFlagBit = static_cast((static_cast(face->face_flags) >> 5) & 1u); + const bool hasSeedVector = obj->layout_seed_vec[0] != 0 || obj->layout_seed_vec[1] != 0; + outParams[2] = static_cast(0xF0u | (hasSeedVector ? 0x08u : 0u) | faceFlagBit); + outParams[3] = 0; + WriteU16(outParams, 0x04, static_cast(face->units_per_EM)); + WritePointer(outParams, 0x10, outlineBlob); + WritePointer(outParams, 0x18, outMetrics); + float styleShiftX = 0.0f; + float styleShiftY = 0.0f; + if (obj->reserved_0x04 != 0) { + styleShiftX = static_cast(slot->metrics.vertBearingX - slot->metrics.horiBearingX); + styleShiftY = static_cast(-(slot->metrics.horiBearingY + slot->metrics.vertBearingY)); + outParams[1] = 1; + } else { + outParams[1] = 0; + } + StoreFloat(outParams, 0x20, styleShiftX); + StoreFloat(outParams, 0x24, styleShiftY); + StoreFloat(outParams, 0x28, 1.0f); + const auto shiftX = static_cast(obj->shift_cache_x); + const auto shiftY = static_cast(obj->shift_cache_y); + const auto seedX = static_cast(obj->layout_seed_vec[0]); + const auto seedY = static_cast(obj->layout_seed_vec[1]); + WriteS32(outParams, 0x2C, shiftX); + WriteS32(outParams, 0x30, shiftY); + WriteS32(outParams, 0x34, seedX); + WriteS32(outParams, 0x38, seedY); + StoreFloat(outParams, 0x3C, static_cast(static_cast(obj->cached_units_x_0x68)) * ONE_OVER_64); + StoreFloat(outParams, 0x40, static_cast(static_cast(obj->cached_units_y_0x70)) * ONE_OVER_64); + WriteU32(outParams, 0x44, 0); + auto xScale = static_cast(face->size->metrics.x_scale); + auto yScale = static_cast(face->size->metrics.y_scale); + const auto objScaleX = static_cast(obj->layout_scale_vec[0]); + const auto objScaleY = static_cast(obj->layout_scale_vec[1]); + if (objScaleX != 0 && objScaleX != 0x10000) xScale = (xScale << 16) / objScaleX; + if (objScaleY != 0 && objScaleY != 0x10000) yScale = (yScale << 16) / objScaleY; + const bool applyScale = (mode & 0x0Fu) != 0; + const auto scaleIf = [&](std::int64_t value, std::int64_t scale) { + return applyScale ? RoundMulFixed64(value, scale) : value; + }; + const auto widthRaw = static_cast(slot->metrics.width); + const auto heightRaw = static_cast(slot->metrics.height); + const auto horiBearingXRaw = static_cast(slot->metrics.horiBearingX); + const auto horiAdvanceRaw = static_cast(slot->metrics.horiAdvance); + const auto vertBearingXRaw = static_cast(slot->metrics.vertBearingX); + const auto vertBearingYRaw = static_cast(slot->metrics.vertBearingY); + const auto vertAdvanceRaw = static_cast(slot->metrics.vertAdvance); + const std::int64_t horiAdvanceUnscaled = seedX + horiAdvanceRaw; + const std::int64_t vertAdvanceUnscaled = seedY + vertAdvanceRaw; + slot->metrics.horiAdvance = static_cast(horiAdvanceUnscaled); + slot->metrics.vertAdvance = static_cast(vertAdvanceUnscaled); + if (widthRaw == 0 && heightRaw == 0) { + *outMetrics = {}; + outMetrics->Horizontal.advance = FromF26Dot6(scaleIf(horiAdvanceUnscaled, xScale)); + outMetrics->Vertical.advance = FromF26Dot6(scaleIf(vertAdvanceUnscaled, yScale)); + return SCE_FONT_OK; + } + const std::int64_t topShift = heightRaw == 0 ? 0 : shiftY; + std::int64_t left = horiBearingXRaw + shiftX; + std::int64_t top = vertBearingYRaw + topShift; + std::int64_t right = left + widthRaw; + std::int64_t bottom = top - heightRaw; + left = scaleIf(left, xScale); + top = scaleIf(top, yScale); + right = scaleIf(right, xScale); + bottom = scaleIf(bottom, yScale); + const std::int64_t horiAdvance = scaleIf(horiAdvanceUnscaled, xScale); + const std::int64_t vertAdvance = scaleIf(vertAdvanceUnscaled, yScale); + const std::int64_t outWidth = right - left; + const std::int64_t outHeight = top - bottom; + std::int64_t vertBearingX; + std::int64_t vertBearingY; + if ((static_cast(face->face_flags) & 0x20u) == 0) { + vertBearingX = left - horiAdvance / 2; + vertBearingY = (vertAdvance - outHeight) / 2; + } else { + if (seedX == 0) { + vertBearingX = scaleIf(shiftX + vertBearingXRaw, xScale); + } else { + slot->metrics.vertBearingX = static_cast(horiBearingXRaw - horiAdvanceUnscaled / 2); + vertBearingX = left - horiAdvance / 2; + StoreFloat(outParams, 0x20, static_cast(-(horiAdvanceUnscaled / 2))); + } + vertBearingY = scaleIf(topShift + vertBearingYRaw, yScale); + } + outMetrics->width = FromF26Dot6(outWidth); + outMetrics->height = FromF26Dot6(outHeight); + outMetrics->Horizontal.bearingX = FromF26Dot6(left); + outMetrics->Horizontal.bearingY = FromF26Dot6(top); + outMetrics->Horizontal.advance = FromF26Dot6(horiAdvance); + outMetrics->Vertical.bearingX = FromF26Dot6(vertBearingX); + outMetrics->Vertical.bearingY = FromF26Dot6(vertBearingY); + outMetrics->Vertical.advance = FromF26Dot6(vertAdvance); + if ((mode & 0x06u) != 0 && obj->font_handle) { + const auto* handle = reinterpret_cast(obj->font_handle); + const StyleStateBlock* style = (mode & 0x04u) ? &handle->cached_style.state : &handle->style; + const float slantRatio = style->slant_ratio; + if ((slantRatio != 0.0f || std::isnan(slantRatio)) && slot->outline.points && slot->outline.n_points > 0) { + const auto shear = static_cast(TruncateFloatToInt(slantRatio * 65536.0f)); + std::int64_t minX = std::numeric_limits::max(); + std::int64_t maxX = std::numeric_limits::min(); + for (int i = 0; i < slot->outline.n_points; ++i) { + const FT_Vector point = slot->outline.points[i]; + const std::int64_t px = scaleIf(static_cast(point.x) + shiftX, xScale); + const std::int64_t py = scaleIf(static_cast(point.y) + shiftY, yScale); + const std::int64_t slantedX = px + RoundMulFixed64(py, shear); + minX = std::min(minX, slantedX); + maxX = std::max(maxX, slantedX); + } + if (minX <= maxX) { + outMetrics->width = FromF26Dot6(maxX - minX); + outMetrics->Horizontal.bearingX = FromF26Dot6(minX); + } + } + const float effectX = style->effect_weight_x; + const float effectY = style->effect_weight_y; + if (effectX != 0.0f || std::isnan(effectX) || effectY != 0.0f || std::isnan(effectY)) { + const float dx = effectX * obj->scale_x_0x50 * 0.5f; + const float dy = effectY * obj->scale_y_0x54 * 0.5f; + outMetrics->width = outMetrics->width + dx + dx; + outMetrics->height = outMetrics->height + dy + dy; + outMetrics->Horizontal.bearingX -= dx; + outMetrics->Horizontal.bearingY += dy; + outMetrics->Vertical.bearingX -= dx; + outMetrics->Vertical.bearingY -= dy; + if (outMetrics->Horizontal.advance != 0.0f || std::isnan(outMetrics->Horizontal.advance)) outMetrics->Horizontal.advance += dx; + if (outMetrics->Vertical.advance != 0.0f || std::isnan(outMetrics->Vertical.advance)) outMetrics->Vertical.advance += dy; + } + } + return SCE_FONT_OK; +} + +int APS5_VABI LibraryApplyGlyphAdjust(FontObj* obj, std::uint32_t p2, std::uint32_t glyphIndex, std::int32_t p4, std::int32_t p5, std::uint32_t* inoutGlyphIndex) { + (void)p2; + if (!obj || !inoutGlyphIndex) return SCE_FONT_ERROR_FATAL; + *inoutGlyphIndex = glyphIndex; + obj->shift_units_x = p4; + obj->shift_units_y = p5; + obj->layout_seed_pair = 0; + return SCE_FONT_OK; +} + +int APS5_VABI LibraryConfigureGlyph(FontObj* obj, std::uint32_t* inParams, std::int32_t mode, std::uint32_t* inoutState) { + if (!obj || !inParams || !inoutState) return SCE_FONT_ERROR_FATAL; + if (*inParams != 0) reinterpret_cast(inoutState)[3] |= 0x80; + obj->glyph_cfg_word_0x130 = *inParams; + obj->glyph_cfg_mode_0x134 = static_cast(mode); + obj->glyph_cfg_byte_0x136 = 0; + obj->glyph_cfg_byte_0x135 = 0; + return SCE_FONT_OK; +} + +int APS5_VABI RendererCreate(RendererNative* renderer) { + if (!renderer) return SCE_FONT_ERROR_INVALID_RENDERER; + auto* ft = reinterpret_cast(renderer); + ft->ft_backend.renderer_header_0x10 = &ft->base.mem_kind; + ft->ft_backend.unknown_0x08 = 0; + ft->ft_backend.unknown_0x10 = 0; + ft->ft_backend.unknown_0x18 = 0; + ft->ft_backend.initialized_marker = ft; + return SCE_FONT_OK; +} + +int APS5_VABI RendererDestroy(RendererNative* renderer) { + if (!renderer) return SCE_FONT_ERROR_INVALID_RENDERER; + reinterpret_cast(renderer)->ft_backend.initialized_marker = nullptr; + return SCE_FONT_OK; +} + +std::uint64_t APS5_VABI RendererQuery(RendererNative* renderer, std::uint8_t* params, std::int64_t* outPtr, std::uint8_t* outVector) { + (void)renderer; + (void)params; + if (outVector) std::memset(outVector, 0, 16); + if (outPtr) *outPtr = 0; + return 0; +} + +SysDriver MakeDriverTable() { + SysDriver driver{}; + driver.pixel_resolution = &LibraryGetPixelResolution; + driver.init = &LibraryInit; + driver.term = &LibraryTerm; + driver.support_formats = &LibrarySupport; + driver.open = &LibraryOpenFont; + driver.close = &LibraryCloseFont; + driver.scale = &LibraryGetFaceScale; + driver.metric = &LibraryGetFaceMetric; + driver.glyph_index = &LibraryGetGlyphIndex; + driver.set_char_with_dpi = &LibrarySetCharSizeWithDpi; + driver.set_char_default_dpi = &LibrarySetCharSizeDefaultDpi; + driver.compute_layout = &LibraryComputeLayout; + driver.compute_layout_alt = &LibraryComputeLayoutAlt; + driver.load_glyph_cached = &LibraryLoadGlyphCached; + driver.get_glyph_metrics = &LibraryGetGlyphMetrics; + driver.apply_glyph_adjust = &LibraryApplyGlyphAdjust; + driver.configure_glyph = &LibraryConfigureGlyph; + return driver; +} + +const SysDriver driverTable = MakeDriverTable(); +const RendererSelection rendererTable{0, sizeof(RendererFt), &RendererCreate, &RendererDestroy, &RendererQuery}; + +} + +const Font::SysDriver* FontFt::DriverTable() { + return &driverTable; +} + +const Font::RendererSelection* FontFt::RendererTable() { + return &rendererTable; +} diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp new file mode 100644 index 000000000..36d1fbf9c --- /dev/null +++ b/core/libs/tests/GuestFont.cpp @@ -0,0 +1,75 @@ +#include "prx/libSceFont/include/FontTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceFontMemoryInit(FontMemory*, void*, std::uint32_t, const FontMemoryInterface*, void*, FontMemoryDestroyFunction, void*); +int APS5_VABI sceFontMemoryTerm(FontMemory*); +int APS5_VABI sceFontCreateLibrary(const FontMemory*, const void*, FontLibrary*); +int APS5_VABI sceFontDestroyLibrary(FontLibrary*); +int APS5_VABI sceFontCreateRenderer(const FontMemory*, const void*, FontRenderer*); +int APS5_VABI sceFontDestroyRenderer(FontRenderer*); +int APS5_VABI sceFontSupportSystemFonts(FontLibrary); +int APS5_VABI sceFontSupportExternalFonts(FontLibrary, std::uint32_t, std::uint32_t); +int APS5_VABI sceFontOpenFontSet(FontLibrary, std::uint32_t, std::uint32_t, const FontOpenDetail*, FontHandle*); +int APS5_VABI sceFontOpenFontMemory(FontLibrary, const void*, std::uint32_t, const FontOpenDetail*, FontHandle*); +const void* APS5_VABI sceFontSelectLibraryFt(int); +const void* APS5_VABI sceFontSelectRendererFt(int); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Font check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static int allocations = 0; +static void* APS5_VABI Allocate(void*, std::uint32_t size) { + ++allocations; + return std::malloc(size); +} +static void APS5_VABI Release(void*, void* pointer) { + if (pointer) --allocations; + std::free(pointer); +} + +int main() { + constexpr std::uint32_t SystemFontSet = 0x18070043u; + const FontMemoryInterface iface{Allocate, Release, nullptr, nullptr, nullptr, nullptr}; + FontMemory memory{}; + Require(sceFontMemoryInit(&memory, nullptr, 0, &iface, nullptr, nullptr, nullptr) == SCE_FONT_OK); + Require(sceFontSelectLibraryFt(0) != nullptr && sceFontSelectLibraryFt(1) == nullptr); + Require(sceFontSelectRendererFt(0) != nullptr && sceFontSelectRendererFt(1) == nullptr); + + FontLibrary library = nullptr; + Require(sceFontCreateLibrary(&memory, nullptr, &library) == SCE_FONT_ERROR_INVALID_PARAMETER && library == nullptr); + Require(sceFontCreateLibrary(&memory, sceFontSelectLibraryFt(0), &library) == SCE_FONT_OK && library != nullptr); + + FontHandle font = reinterpret_cast(&memory); + Require(sceFontOpenFontSet(library, SystemFontSet, 1, nullptr, &font) == SCE_FONT_ERROR_NO_SUPPORT_FUNCTION && font == nullptr); + Require(sceFontSupportSystemFonts(library) == SCE_FONT_OK); + Require(sceFontOpenFontSet(library, SystemFontSet, 1, nullptr, &font) == SCE_FONT_ERROR_FONT_OPEN_FAILED && font == nullptr); + Require(sceFontOpenFontSet(library, 0x12345678u, 1, nullptr, &font) == SCE_FONT_ERROR_NO_SUPPORT_FONTSET); + Require(sceFontOpenFontSet(library, SystemFontSet, 7, nullptr, &font) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontOpenFontSet(nullptr, SystemFontSet, 1, nullptr, &font) == SCE_FONT_ERROR_INVALID_LIBRARY); + const unsigned char notAFont[64] = {1, 2, 3, 4}; + Require(sceFontOpenFontMemory(library, notAFont, sizeof(notAFont), nullptr, &font) == SCE_FONT_ERROR_NO_SUPPORT_FUNCTION && font == nullptr); + Require(sceFontSupportExternalFonts(library, 4, 0x52) == SCE_FONT_OK); + Require(sceFontSupportExternalFonts(library, 4, 0x52) == SCE_FONT_ERROR_ALREADY_SPECIFIED); + Require(sceFontOpenFontMemory(library, nullptr, 0, nullptr, &font) == SCE_FONT_ERROR_INVALID_PARAMETER && font == nullptr); + Require(sceFontOpenFontMemory(library, notAFont, sizeof(notAFont), nullptr, &font) == SCE_FONT_ERROR_NO_SUPPORT_FORMAT && font == nullptr); + + FontRenderer renderer = nullptr; + Require(sceFontCreateRenderer(&memory, sceFontSelectRendererFt(0), &renderer) == SCE_FONT_OK && renderer != nullptr); + Require(sceFontDestroyRenderer(&renderer) == SCE_FONT_OK && renderer == nullptr); + Require(sceFontDestroyRenderer(&renderer) == SCE_FONT_ERROR_INVALID_RENDERER); + + Require(sceFontDestroyLibrary(&library) == SCE_FONT_OK && library == nullptr); + Require(sceFontDestroyLibrary(&library) == SCE_FONT_ERROR_INVALID_LIBRARY); + Require(allocations == 0); + Require(sceFontMemoryTerm(&memory) == SCE_FONT_OK); + Require(sceFontMemoryTerm(&memory) == SCE_FONT_ERROR_INVALID_MEMORY); +} From 588b5bb2bf97f10497aa880f200b23cbbd8ec510 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 28 Sep 2026 22:03:39 +1300 Subject: [PATCH 06/46] build(font): build FreeType from a submodule instead of requiring a system package Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01ButHENCbXXt8h4EZuWUabJ --- .gitmodules | 3 +++ 3rdparty/freetype | 1 + CMakeLists.txt | 9 +++++++++ core/libs/prx/libSceFont/FontLibrary.cmake | 4 +--- core/libs/prx/libSceFontFt/CMakeLists.txt | 4 +--- 5 files changed, 15 insertions(+), 6 deletions(-) create mode 160000 3rdparty/freetype diff --git a/.gitmodules b/.gitmodules index a4bbf61a1..3c7f04fbe 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,6 @@ [submodule "3rdparty/LibAtrac9"] path = 3rdparty/LibAtrac9 url = https://github.com/shadps4-emu/ext-LibAtrac9.git +[submodule "3rdparty/freetype"] + path = 3rdparty/freetype + url = https://github.com/freetype/freetype.git diff --git a/3rdparty/freetype b/3rdparty/freetype new file mode 160000 index 000000000..42608f77f --- /dev/null +++ b/3rdparty/freetype @@ -0,0 +1 @@ +Subproject commit 42608f77f20749dd6ddc9e0536788eaad70ea4b5 diff --git a/CMakeLists.txt b/CMakeLists.txt index 30724ac32..0c156dd83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,15 @@ set(CMAKE_EXE_LINKER_FLAGS "") add_subdirectory(3rdparty/SDL2) set(CMAKE_EXE_LINKER_FLAGS "${ANYPS5_EXE_LINKER_FLAGS}") +set(FT_DISABLE_ZLIB ON CACHE BOOL "" FORCE) +set(FT_DISABLE_BZIP2 ON CACHE BOOL "" FORCE) +set(FT_DISABLE_PNG ON CACHE BOOL "" FORCE) +set(FT_DISABLE_HARFBUZZ ON CACHE BOOL "" FORCE) +set(FT_DISABLE_BROTLI ON CACHE BOOL "" FORCE) +set(SKIP_INSTALL_ALL ON CACHE BOOL "" FORCE) +add_subdirectory(3rdparty/freetype EXCLUDE_FROM_ALL) +set_target_properties(freetype PROPERTIES POSITION_INDEPENDENT_CODE ON) + add_subdirectory(core/shader/recompiler) add_subdirectory(core/libs) add_subdirectory(core/relinker) diff --git a/core/libs/prx/libSceFont/FontLibrary.cmake b/core/libs/prx/libSceFont/FontLibrary.cmake index 36f76410a..26e287f39 100644 --- a/core/libs/prx/libSceFont/FontLibrary.cmake +++ b/core/libs/prx/libSceFont/FontLibrary.cmake @@ -1,5 +1,3 @@ -find_package(Freetype REQUIRED) - function(add_sce_font_library target) set(fontDir ${CMAKE_CURRENT_FUNCTION_LIST_DIR}) add_library(${target} SHARED EXCLUDE_FROM_ALL @@ -13,7 +11,7 @@ function(add_sce_font_library target) ${fontDir}/src/Unimplemented.cpp ) target_include_directories(${target} PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(${target} PRIVATE Freetype::Freetype libc) + target_link_libraries(${target} PRIVATE freetype libc) set_target_properties(${target} PROPERTIES CXX_EXTENSIONS OFF CXX_VISIBILITY_PRESET hidden diff --git a/core/libs/prx/libSceFontFt/CMakeLists.txt b/core/libs/prx/libSceFontFt/CMakeLists.txt index c1fa26487..6428668cd 100644 --- a/core/libs/prx/libSceFontFt/CMakeLists.txt +++ b/core/libs/prx/libSceFontFt/CMakeLists.txt @@ -1,12 +1,10 @@ -find_package(Freetype REQUIRED) - add_library(libSceFontFt SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Driver.cpp ) target_include_directories(libSceFontFt PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(libSceFontFt PRIVATE Freetype::Freetype libc) +target_link_libraries(libSceFontFt PRIVATE freetype libc) set_target_properties(libSceFontFt PROPERTIES CXX_EXTENSIONS OFF CXX_VISIBILITY_PRESET hidden From 80a64ccd6b4799d34de715c74134701d2e2b099c Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Mon, 28 Sep 2026 19:35:18 +1000 Subject: [PATCH 07/46] fix(shader): distinguish IR values with different instruction flags --- .../recompiler/IntermediateRepresentation/src/IrValue.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp index c9366915a..3a0cabd76 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp @@ -261,7 +261,7 @@ bool IrValue::operator==(const IrValue& other) const { if (this == &other) { return true; } - if (opcode != other.opcode || type != other.type || hasImmediate != other.hasImmediate) { + if (opcode != other.opcode || type != other.type || flags != other.flags || hasImmediate != other.hasImmediate) { return false; } if (hasImmediate && immediateBits != other.immediateBits) { From 730de17774aa73b63747d2ac311ab4dbc5bd14bb Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Mon, 28 Sep 2026 19:35:18 +1000 Subject: [PATCH 08/46] fix(shader): support V_PERM_B32 byte selection --- .../include/RdnaDecoder/RdnaOpcode.hpp | 1 + .../recompiler/RdnaDecoder/src/RdnaOpcode.cpp | 1 + .../RdnaDecoder/src/RdnaVectorOpDecoder.cpp | 1 + .../Translation/TranslationContext.hpp | 1 + .../Translation/src/IntegerInstructions.cpp | 22 +++++++++++++++++++ .../Translation/src/VectorInstructions.cpp | 2 ++ 6 files changed, 28 insertions(+) diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp index 76b6972ac..f06f8d362 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp @@ -284,6 +284,7 @@ enum class RdnaOpcode : std::uint16_t { VAdd3U32, VLshlAddU32, VAddLshlU32, + VPermB32, VXadU32, VLshlOrB32, VAndOrB32, diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaOpcode.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaOpcode.cpp index f77f40a5e..54ecc199c 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaOpcode.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaOpcode.cpp @@ -257,6 +257,7 @@ bool IsVectorAluOpcode(RdnaOpcode opcode) { case RdnaOpcode::VAdd3U32: case RdnaOpcode::VLshlAddU32: case RdnaOpcode::VAddLshlU32: + case RdnaOpcode::VPermB32: case RdnaOpcode::VXadU32: case RdnaOpcode::VLshlOrB32: case RdnaOpcode::VAndOrB32: diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp index 34326cc79..cb3fb15f1 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -355,6 +355,7 @@ constexpr VectorOpcodeInfo vop3Opcodes[] = { {0x311u, RdnaOpcode::VPackB32F16}, {0x314u, RdnaOpcode::VLshlrevB16}, {0x319u, RdnaOpcode::VSubrevI32}, + {0x344u, RdnaOpcode::VPermB32}, {0x345u, RdnaOpcode::VXadU32}, {0x346u, RdnaOpcode::VLshlAddU32}, {0x347u, RdnaOpcode::VAddLshlU32}, diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index 2aa9f3c36..3a5f8d709 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -186,6 +186,7 @@ private: bool vAlignbyteB32(const RdnaInstruction& inst); bool vLshlAddU32(const RdnaInstruction& inst); bool vAddLshlU32(const RdnaInstruction& inst); + bool vPermB32(const RdnaInstruction& inst); bool vXadU32(const RdnaInstruction& inst); bool vLshlOrB32(const RdnaInstruction& inst); bool vCndmaskB32(const RdnaInstruction& inst); diff --git a/core/shader/recompiler/Translation/src/IntegerInstructions.cpp b/core/shader/recompiler/Translation/src/IntegerInstructions.cpp index 7a3dffd48..310dae951 100644 --- a/core/shader/recompiler/Translation/src/IntegerInstructions.cpp +++ b/core/shader/recompiler/Translation/src/IntegerInstructions.cpp @@ -532,6 +532,28 @@ bool TranslationContext::vAddLshlU32(const RdnaInstruction& inst) { return true; } +bool TranslationContext::vPermB32(const RdnaInstruction& inst) { + const IrU32 high = readU32(sourceAt(inst, 0u)); + const IrU32 low = readU32(sourceAt(inst, 1u)); + const IrU32 selectors = readU32(sourceAt(inst, 2u)); + IrValue* result = &ir.Constant(0u); + for (std::uint32_t byte = 0u; byte < 4u; ++byte) { + IrValue& selector = ir.BitwiseAnd(ir.ShiftRightLogical(selectors.Value(), ir.Constant(byte * 8u)), ir.Constant(255u)); + IrValue& signIndex = ir.IAdd(ir.IMul(ir.BitwiseAnd(selector, ir.Constant(3u)), ir.Constant(2u)), ir.Constant(1u)); + IrValue& index = ir.Select(ir.ULessThan(selector, ir.Constant(8u)), selector, signIndex); + IrValue& word = ir.Select(ir.ULessThan(index, ir.Constant(4u)), low.Value(), high.Value()); + IrValue& shift = ir.IMul(ir.BitwiseAnd(index, ir.Constant(3u)), ir.Constant(8u)); + IrValue& value = ir.BitwiseAnd(ir.ShiftRightLogical(word, shift), ir.Constant(255u)); + IrValue& sign = ir.Select(ir.UGreaterThan(value, ir.Constant(127u)), ir.Constant(255u), ir.Constant(0u)); + IrValue& selected = ir.Select(ir.ULessThan(selector, ir.Constant(8u)), value, sign); + IrValue& fill = ir.Select(ir.IEqual(selector, ir.Constant(12u)), ir.Constant(0u), ir.Constant(255u)); + IrValue& output = ir.Select(ir.ULessThan(selector, ir.Constant(12u)), selected, fill); + result = &ir.BitwiseOr(*result, ir.ShiftLeftLogical(output, ir.Constant(byte * 8u))); + } + writeOperand(inst.destination, result); + return true; +} + bool TranslationContext::vXadU32(const RdnaInstruction& inst) { const IrU32 lhs = readU32(sourceAt(inst, 0u)); const IrU32 rhs = readU32(sourceAt(inst, 1u)); diff --git a/core/shader/recompiler/Translation/src/VectorInstructions.cpp b/core/shader/recompiler/Translation/src/VectorInstructions.cpp index 61eba45b1..ff327c922 100644 --- a/core/shader/recompiler/Translation/src/VectorInstructions.cpp +++ b/core/shader/recompiler/Translation/src/VectorInstructions.cpp @@ -673,6 +673,8 @@ bool TranslationContext::emitVector(const RdnaInstruction& inst) { return vLshlAddU32(inst); case RdnaOpcode::VAddLshlU32: return vAddLshlU32(inst); + case RdnaOpcode::VPermB32: + return vPermB32(inst); case RdnaOpcode::VXadU32: return vXadU32(inst); case RdnaOpcode::VLshlOrB32: From 8fac768b9c63a810d1a5d8726999085b55031cea Mon Sep 17 00:00:00 2001 From: Julien Legay Date: Mon, 28 Sep 2026 11:45:04 +0200 Subject: [PATCH 09/46] feat(libSceMouse): implement stateful mouse API exports Keep the existing Export.cpp entry point and System V ABI while moving mouse state into the library implementation.`n`nTrack initialization, a single handle, button state, timestamps, and a bounded non-blocking event queue. Define and assert the guest MouseData layout and expose an input publisher for the host backend. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- core/libs/prx/libSceMouse/CMakeLists.txt | 7 ++ core/libs/prx/libSceMouse/Export.cpp | 37 +++--- .../prx/libSceMouse/include/MouseState.hpp | 28 +++++ .../prx/libSceMouse/include/mouse_structs.h | 34 ++++++ core/libs/prx/libSceMouse/src/mouse_impl.cpp | 106 ++++++++++++++++++ 5 files changed, 188 insertions(+), 24 deletions(-) create mode 100644 core/libs/prx/libSceMouse/CMakeLists.txt create mode 100644 core/libs/prx/libSceMouse/include/MouseState.hpp create mode 100644 core/libs/prx/libSceMouse/include/mouse_structs.h create mode 100644 core/libs/prx/libSceMouse/src/mouse_impl.cpp diff --git a/core/libs/prx/libSceMouse/CMakeLists.txt b/core/libs/prx/libSceMouse/CMakeLists.txt new file mode 100644 index 000000000..99c3cbbfc --- /dev/null +++ b/core/libs/prx/libSceMouse/CMakeLists.txt @@ -0,0 +1,7 @@ +add_library(libSceMouse SHARED EXCLUDE_FROM_ALL + ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/mouse_impl.cpp +) +target_include_directories(libSceMouse PRIVATE ${LIBS_INCLUDE_DIR}) +set_target_properties(libSceMouse PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(libSceMouse) diff --git a/core/libs/prx/libSceMouse/Export.cpp b/core/libs/prx/libSceMouse/Export.cpp index 5e6120682..9d820d245 100644 --- a/core/libs/prx/libSceMouse/Export.cpp +++ b/core/libs/prx/libSceMouse/Export.cpp @@ -1,36 +1,25 @@ -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" +#include "prx/libSceMouse/include/MouseState.hpp" extern "C" { -int APS5_VABI sceMouseClose(int32_t handle) { - (void)handle; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceMouseInit() { + return Mouse::Initialize(); } -int APS5_VABI sceMouseInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceMouseOpen(int userId, std::int32_t type, std::int32_t index, const void* param) { + return Mouse::Open(userId, type, index, static_cast(param)); } -int APS5_VABI sceMouseOpen(int user_id, int32_t type, int32_t index, const void* param) { - (void)user_id; - (void)type; - (void)index; - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceMouseClose(std::int32_t handle) { + return Mouse::Close(handle); } -int APS5_VABI sceMouseRead(int32_t handle, MouseData* data, int32_t num) { - (void)handle; - (void)data; - (void)num; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceMouseRead(std::int32_t handle, MouseData* data, std::int32_t num) { + return Mouse::Read(handle, data, num); +} + +void MousePublishInput_nid_postfix(const MouseInputEvent& event) { + Mouse::Publish(event); } } diff --git a/core/libs/prx/libSceMouse/include/MouseState.hpp b/core/libs/prx/libSceMouse/include/MouseState.hpp new file mode 100644 index 000000000..bf7d57a09 --- /dev/null +++ b/core/libs/prx/libSceMouse/include/MouseState.hpp @@ -0,0 +1,28 @@ +#ifndef CORE_LIBS_PRX_LIBSCEMOUSE_MOUSESTATE_HPP +#define CORE_LIBS_PRX_LIBSCEMOUSE_MOUSESTATE_HPP + +#include "prx/libSceMouse/include/mouse_structs.h" + +struct MouseInputEvent { + std::int32_t x = 0; + std::int32_t y = 0; + std::int32_t wheel = 0; + std::int32_t tilt = 0; + std::uint32_t button = 0; + bool pressed = false; + bool resetButtons = false; + bool connectionChange = false; + bool connected = true; +}; + +namespace Mouse { +int Initialize(); +int Open(int userId, std::int32_t type, std::int32_t index, const MouseOpenParam* param); +int Close(std::int32_t handle); +int Read(std::int32_t handle, MouseData* data, std::int32_t num); +void Publish(const MouseInputEvent& event); +} + +extern "C" void MousePublishInput_nid_postfix(const MouseInputEvent& event); + +#endif diff --git a/core/libs/prx/libSceMouse/include/mouse_structs.h b/core/libs/prx/libSceMouse/include/mouse_structs.h new file mode 100644 index 000000000..30c7d3a8f --- /dev/null +++ b/core/libs/prx/libSceMouse/include/mouse_structs.h @@ -0,0 +1,34 @@ +#ifndef CORE_LIBS_PRX_LIBSCEMOUSE_MOUSE_STRUCTS_H +#define CORE_LIBS_PRX_LIBSCEMOUSE_MOUSE_STRUCTS_H + +#include +#include +#include "SceTypes.hpp" + +struct MouseOpenParam { + std::uint8_t behaviorFlag; + std::uint8_t reserved[7]; +}; + +constexpr int MOUSE_OK = 0; +constexpr int MOUSE_ERROR_INVALID_ARG = static_cast(0x80df0001u); +constexpr int MOUSE_ERROR_INVALID_HANDLE = static_cast(0x80df0003u); +constexpr int MOUSE_ERROR_ALREADY_OPENED = static_cast(0x80df0004u); +constexpr int MOUSE_ERROR_NOT_INITIALIZED = static_cast(0x80df0005u); +constexpr int MOUSE_MAX_DATA_NUM = 64; +constexpr int MOUSE_HANDLE = 1; +constexpr std::uint8_t MOUSE_OPEN_PARAM_MERGED = 1; + +static_assert(sizeof(MouseOpenParam) == 8); +static_assert(sizeof(MouseData) == 40); +static_assert(alignof(MouseData) == 8); +static_assert(offsetof(MouseData, timestamp) == 0); +static_assert(offsetof(MouseData, connected) == 8); +static_assert(offsetof(MouseData, buttons) == 12); +static_assert(offsetof(MouseData, x_axis) == 16); +static_assert(offsetof(MouseData, y_axis) == 20); +static_assert(offsetof(MouseData, wheel) == 24); +static_assert(offsetof(MouseData, tilt) == 28); +static_assert(offsetof(MouseData, reserved) == 32); + +#endif diff --git a/core/libs/prx/libSceMouse/src/mouse_impl.cpp b/core/libs/prx/libSceMouse/src/mouse_impl.cpp new file mode 100644 index 000000000..5bcf78e72 --- /dev/null +++ b/core/libs/prx/libSceMouse/src/mouse_impl.cpp @@ -0,0 +1,106 @@ +#include "prx/libSceMouse/include/MouseState.hpp" +#include "prx/libkernel/Time/include/Time.hpp" + +#include +#include +#include + +namespace { +std::mutex mouseMutex; +std::array history{}; +int head = 0; +int count = 0; +bool initialized = false; +bool opened = false; +bool connected = true; +std::uint32_t buttons = 0; + +void enqueue(const MouseData& data) { + if (count == MOUSE_MAX_DATA_NUM) { + head = (head + 1) % MOUSE_MAX_DATA_NUM; + --count; + } + history[(head + count) % MOUSE_MAX_DATA_NUM] = data; + ++count; +} +} + +namespace Mouse { + +int Initialize() { + std::lock_guard lock(mouseMutex); + initialized = true; + return MOUSE_OK; +} + +int Open(int userId, std::int32_t type, std::int32_t index, const MouseOpenParam* param) { + std::lock_guard lock(mouseMutex); + if (!initialized) return MOUSE_ERROR_NOT_INITIALIZED; + if (userId < 0 || type != 0 || index != 0 || + (param != nullptr && (param->behaviorFlag & ~MOUSE_OPEN_PARAM_MERGED) != 0)) { + return MOUSE_ERROR_INVALID_ARG; + } + if (opened) return MOUSE_ERROR_ALREADY_OPENED; + opened = true; + buttons = 0; + head = 0; + count = 0; + MouseData initial{}; + initial.timestamp = sceKernelGetProcessTime(); + initial.connected = connected; + enqueue(initial); + return MOUSE_HANDLE; +} + +int Close(std::int32_t handle) { + std::lock_guard lock(mouseMutex); + if (!initialized) return MOUSE_ERROR_NOT_INITIALIZED; + if (handle != MOUSE_HANDLE || !opened) return MOUSE_ERROR_INVALID_HANDLE; + opened = false; + buttons = 0; + head = 0; + count = 0; + return MOUSE_OK; +} + +int Read(std::int32_t handle, MouseData* data, std::int32_t num) { + std::lock_guard lock(mouseMutex); + if (!initialized) return MOUSE_ERROR_NOT_INITIALIZED; + if (handle != MOUSE_HANDLE || !opened) return MOUSE_ERROR_INVALID_HANDLE; + if (data == nullptr || num <= 0 || num > MOUSE_MAX_DATA_NUM) return MOUSE_ERROR_INVALID_ARG; + const int available = std::min(count, num); + for (int i = 0; i < available; ++i) { + data[i] = history[head]; + head = (head + 1) % MOUSE_MAX_DATA_NUM; + } + count -= available; + return available; +} + +void Publish(const MouseInputEvent& event) { + std::lock_guard lock(mouseMutex); + if (event.connectionChange) { + if (connected == event.connected) return; + connected = event.connected; + if (!connected) buttons = 0; + } else if (!connected) { + return; + } + if (!opened) return; + if (event.resetButtons) buttons = 0; + if (event.button != 0) { + if (event.pressed) buttons |= event.button; + else buttons &= ~event.button; + } + MouseData data{}; + data.timestamp = sceKernelGetProcessTime(); + data.connected = connected; + data.buttons = buttons; + data.x_axis = event.x; + data.y_axis = event.y; + data.wheel = event.wheel; + data.tilt = event.tilt; + enqueue(data); +} + +} From ddb8add7539d826582770c2ebf4bf8e434b3dfce Mon Sep 17 00:00:00 2001 From: Julien Legay Date: Mon, 28 Sep 2026 11:45:31 +0200 Subject: [PATCH 10/46] feat(libSceVideoOut): route SDL mouse events to libSceMouse Forward relative motion, supported button transitions, and horizontal and vertical scrolling from the existing SDL event loop. Reset pressed buttons on focus loss without reporting a device disconnect, and track window close and refocus transitions. Link libSceVideoOut to the mouse library using the existing Pad integration pattern. Add targeted coverage for API errors, event mapping, focus changes, history overflow, and reopening. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- core/libs/prx/libSceMouse/CMakeLists.txt | 15 +++ core/libs/prx/libSceMouse/tests/Mouse.cpp | 108 ++++++++++++++++++ core/libs/prx/libSceVideoOut/CMakeLists.txt | 3 +- .../prx/libSceVideoOut/include/MouseInput.hpp | 14 +++ .../prx/libSceVideoOut/src/MouseInput.cpp | 55 +++++++++ .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 3 + 6 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceMouse/tests/Mouse.cpp create mode 100644 core/libs/prx/libSceVideoOut/include/MouseInput.hpp create mode 100644 core/libs/prx/libSceVideoOut/src/MouseInput.cpp diff --git a/core/libs/prx/libSceMouse/CMakeLists.txt b/core/libs/prx/libSceMouse/CMakeLists.txt index 99c3cbbfc..76b0c4fe8 100644 --- a/core/libs/prx/libSceMouse/CMakeLists.txt +++ b/core/libs/prx/libSceMouse/CMakeLists.txt @@ -5,3 +5,18 @@ add_library(libSceMouse SHARED EXCLUDE_FROM_ALL target_include_directories(libSceMouse PRIVATE ${LIBS_INCLUDE_DIR}) set_target_properties(libSceMouse PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(libSceMouse) + +if(BUILD_TESTING) + add_executable(mouse_tests + ${CMAKE_CURRENT_SOURCE_DIR}/tests/Mouse.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libSceVideoOut/src/MouseInput.cpp + ) + target_include_directories(mouse_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include) + target_link_libraries(mouse_tests PRIVATE libSceMouse libkernel libc) + configure_windows_unwind(mouse_tests) + add_test(NAME mouse_input COMMAND mouse_tests) + if(WIN32) + set_tests_properties(mouse_input PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceMouse/tests/Mouse.cpp b/core/libs/prx/libSceMouse/tests/Mouse.cpp new file mode 100644 index 000000000..e01f3f9b4 --- /dev/null +++ b/core/libs/prx/libSceMouse/tests/Mouse.cpp @@ -0,0 +1,108 @@ +#include "prx/libSceMouse/include/mouse_structs.h" +#include "prx/libSceMouse/include/MouseState.hpp" +#include "prx/libSceVideoOut/include/MouseInput.hpp" +#include "SDL_events.h" +#include "SDL_mouse.h" + +#include + +extern "C" { +int APS5_VABI sceMouseInit(); +int APS5_VABI sceMouseOpen(int, std::int32_t, std::int32_t, const void*); +int APS5_VABI sceMouseClose(std::int32_t); +int APS5_VABI sceMouseRead(std::int32_t, MouseData*, std::int32_t); +} + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + MouseData data[64]{}; + Require(sceMouseOpen(1, 0, 0, nullptr) == MOUSE_ERROR_NOT_INITIALIZED); + Require(sceMouseInit() == MOUSE_OK); + Require(sceMouseInit() == MOUSE_OK); + Require(sceMouseOpen(1, 0, 1, nullptr) == MOUSE_ERROR_INVALID_ARG); + MouseOpenParam unsupported{}; + unsupported.behaviorFlag = 2; + Require(sceMouseOpen(1, 0, 0, &unsupported) == MOUSE_ERROR_INVALID_ARG); + Require(sceMouseOpen(1, 0, 0, nullptr) == MOUSE_HANDLE); + Require(sceMouseOpen(1, 0, 0, nullptr) == MOUSE_ERROR_ALREADY_OPENED); + Require(sceMouseRead(42, data, 1) == MOUSE_ERROR_INVALID_HANDLE); + Require(sceMouseRead(MOUSE_HANDLE, nullptr, 1) == MOUSE_ERROR_INVALID_ARG); + Require(sceMouseRead(MOUSE_HANDLE, data, 65) == MOUSE_ERROR_INVALID_ARG); + Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 1); + Require(data[0].connected && data[0].buttons == 0); + Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 0); + + MouseInput input; + SDL_Event event{}; + event.type = SDL_MOUSEMOTION; + event.motion.windowID = 7; + event.motion.xrel = 9; + event.motion.yrel = -4; + input.HandleEvent(event, 8); + Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 0); + input.HandleEvent(event, 7); + + event = {}; + event.type = SDL_MOUSEBUTTONDOWN; + event.button.windowID = 7; + event.button.button = SDL_BUTTON_RIGHT; + input.HandleEvent(event, 7); + event = {}; + event.type = SDL_MOUSEWHEEL; + event.wheel.windowID = 7; + event.wheel.x = 2; + event.wheel.y = -3; + event.wheel.direction = SDL_MOUSEWHEEL_FLIPPED; + input.HandleEvent(event, 7); + Require(sceMouseRead(MOUSE_HANDLE, data, 3) == 3); + Require(data[0].x_axis == 9 && data[0].y_axis == -4 && data[0].buttons == 0); + Require(data[1].buttons == 2); + Require(data[2].buttons == 2 && data[2].wheel == 3 && data[2].tilt == -2); + Require(data[0].timestamp <= data[2].timestamp); + event = {}; + event.type = SDL_MOUSEBUTTONDOWN; + event.button.windowID = 7; + event.button.button = SDL_BUTTON_X1; + input.HandleEvent(event, 7); + Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 0); + + event = {}; + event.type = SDL_WINDOWEVENT; + event.window.windowID = 7; + event.window.event = SDL_WINDOWEVENT_FOCUS_LOST; + input.HandleEvent(event, 7); + Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 1); + Require(data[0].connected && data[0].buttons == 0); + event.type = SDL_MOUSEMOTION; + event.motion.xrel = 5; + input.HandleEvent(event, 7); + Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 0); + event.type = SDL_WINDOWEVENT; + event.window.event = SDL_WINDOWEVENT_FOCUS_GAINED; + input.HandleEvent(event, 7); + Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 0); + event.window.event = SDL_WINDOWEVENT_CLOSE; + input.HandleEvent(event, 7); + Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 1 && !data[0].connected); + event.window.event = SDL_WINDOWEVENT_FOCUS_GAINED; + input.HandleEvent(event, 7); + Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 1 && data[0].connected); + + for (int i = 0; i < 70; ++i) { + MouseInputEvent motion{}; + motion.x = i; + MousePublishInput_nid_postfix(motion); + } + Require(sceMouseRead(MOUSE_HANDLE, data, 64) == 64); + Require(data[0].x_axis == 6 && data[63].x_axis == 69); + Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 0); + Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_OK); + Require(sceMouseRead(MOUSE_HANDLE, data, 1) == MOUSE_ERROR_INVALID_HANDLE); + Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_ERROR_INVALID_HANDLE); + MouseOpenParam merged{}; + merged.behaviorFlag = MOUSE_OPEN_PARAM_MERGED; + Require(sceMouseOpen(1, 0, 0, &merged) == MOUSE_HANDLE); + Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 1 && data[0].buttons == 0); + Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_OK); +} diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index e41cf34bd..e039c0529 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -6,6 +6,7 @@ add_library(libSceVideoOut SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/VideoOutDriver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/DisplayWindow.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/PadInput.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/MouseInput.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/InputMapping.cpp ) @@ -14,7 +15,7 @@ target_include_directories(libSceVideoOut PRIVATE ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include ) -target_link_libraries(libSceVideoOut PRIVATE libSceAgcDriver libScePad libc libkernel SDL2-static) +target_link_libraries(libSceVideoOut PRIVATE libSceAgcDriver libScePad libSceMouse libc libkernel SDL2-static) if(WIN32) target_link_libraries(libSceVideoOut PRIVATE comctl32) endif() diff --git a/core/libs/prx/libSceVideoOut/include/MouseInput.hpp b/core/libs/prx/libSceVideoOut/include/MouseInput.hpp new file mode 100644 index 000000000..56ea31b6d --- /dev/null +++ b/core/libs/prx/libSceVideoOut/include/MouseInput.hpp @@ -0,0 +1,14 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVIDEOOUT_MOUSEINPUT_HPP +#define CORE_LIBS_PRX_LIBSCEVIDEOOUT_MOUSEINPUT_HPP + +#include "SDL_events.h" + +class MouseInput { +public: + void HandleEvent(const SDL_Event& event, unsigned windowId); + +private: + bool focused = true; +}; + +#endif diff --git a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp new file mode 100644 index 000000000..3a5443e6e --- /dev/null +++ b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp @@ -0,0 +1,55 @@ +#include "prx/libSceVideoOut/include/MouseInput.hpp" +#include "prx/libSceMouse/include/MouseState.hpp" + +#include "SDL_mouse.h" + +void MouseInput::HandleEvent(const SDL_Event& event, unsigned windowId) { + MouseInputEvent input{}; + switch (event.type) { + case SDL_MOUSEMOTION: + if (event.motion.windowID != windowId || !focused) return; + input.x = event.motion.xrel; + input.y = event.motion.yrel; + break; + case SDL_MOUSEBUTTONDOWN: + case SDL_MOUSEBUTTONUP: + if (event.button.windowID != windowId || !focused) return; + switch (event.button.button) { + case SDL_BUTTON_LEFT: input.button = 1; break; + case SDL_BUTTON_RIGHT: input.button = 2; break; + case SDL_BUTTON_MIDDLE: input.button = 4; break; + default: return; + } + input.pressed = event.type == SDL_MOUSEBUTTONDOWN; + break; + case SDL_MOUSEWHEEL: + if (event.wheel.windowID != windowId || !focused) return; + input.wheel = event.wheel.y; + input.tilt = event.wheel.x; + if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) { + input.wheel = -input.wheel; + input.tilt = -input.tilt; + } + if (input.wheel == 0 && input.tilt == 0) return; + break; + case SDL_WINDOWEVENT: + if (event.window.windowID != windowId) return; + if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) { + focused = false; + input.resetButtons = true; + } else if (event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) { + focused = true; + input.connectionChange = true; + } else if (event.window.event == SDL_WINDOWEVENT_CLOSE) { + focused = false; + input.connectionChange = true; + input.connected = false; + } else { + return; + } + break; + default: + return; + } + MousePublishInput_nid_postfix(input); +} diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index d413eab44..82bc028fc 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -9,6 +9,7 @@ #include "SDL.h" #include "SDL_vulkan.h" #include "prx/libSceVideoOut/include/PadInput.hpp" +#include "prx/libSceVideoOut/include/MouseInput.hpp" #include "prx/libScePad/include/PadState.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/Time/include/Time.hpp" @@ -465,6 +466,7 @@ void VideoOutDriver::presentLoop(std::stop_token token) { std::shared_ptr current; try { PadInput padInput; + MouseInput mouseInput; while (!token.stop_requested()) { { std::unique_lock lock(flipQueue->mutex); @@ -480,6 +482,7 @@ void VideoOutDriver::presentLoop(std::stop_token token) { while (SDL_PollEvent(&event)) { require(event.type != SDL_QUIT, "window was closed"); padInput.HandleEvent(event, window); + if (window.Handle() != nullptr) mouseInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); } padInput.Update(); if (current) { From 3230352202db1a36c30458c911b671059c0febe8 Mon Sep 17 00:00:00 2001 From: OverkillLabs <10800190+OverkillLabs@users.noreply.github.com> Date: Mon, 28 Sep 2026 12:45:34 +0300 Subject: [PATCH 11/46] feat(net): offline socket/epoll/resolver stack with FreeBSD errno mapping libSceNet: replace the always-fail stubs with a real offline network stack. Sockets, epoll instances, memory pools and resolvers are tracked in proper registries with create/destroy lifecycles; bind/listen/accept/ connect/send/recv/getsockopt/setsockopt/getsockname behave the way they would on a console with no link up (bind and listen succeed locally, connect/send fail with the errno a routeless host would give, blocking calls such as accept/recv/epoll_wait return on close, abort or their timeout). Infinite ("wait forever") timeouts are serviced in bounded slices so a caller can never deadlock the process. Errno values match the FreeBSD numbering the guest expects through sceNetErrnoLoc. Restored sceNetGetsockopt and sceNetResolverStartAton (present upstream) using the same registry-backed conventions as the rest of the rewrite. libSceRudp: companion change giving the reliable-UDP library a genuine context registry (init/create/bind/set-option/get-status) with consistent error codes; connection attempts consistently fail since no peer can ever be reached offline. --- core/libs/prx/libSceNet/Export.cpp | 777 ++++++++++++++++++---------- core/libs/prx/libSceRudp/Export.cpp | 176 ++++++- 2 files changed, 673 insertions(+), 280 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index bb4b9bab3..53d25dc66 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -1,339 +1,588 @@ -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" #include #include #include +#include #include +#include +#include #include -#include #include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" -// No network is emulated: pools, resolvers and epoll instances exist, but sockets cannot be -// created, so every socket operation fails with the guest's EBADF. -static constexpr int GUEST_EINTR = 4; -static constexpr int GUEST_EBADF = 9; -static constexpr int GUEST_EINVAL = 22; -static constexpr int GUEST_ENOSPC = 28; -static constexpr int GUEST_EAFNOSUPPORT = 47; -static constexpr int GUEST_ENETDOWN = 50; -static constexpr int GUEST_EHOSTUNREACH = 65; -static constexpr int GUEST_AF_INET = 2; +// Offline network stack: no link is ever up. Sockets are local bookkeeping objects (create/bind/listen/options +// succeed like they do on a console without a connection), anything that needs a peer fails with the same errno a +// console with no route gives, and blocking calls return on their timeout, an abort or a close — infinite +// ("wait forever") timeouts are serviced in bounded slices so the inert stack can never deadlock a caller. -static thread_local int g_netErrno = 0; -static std::atomic g_nextId{1}; +namespace { +// FreeBSD errno numbers as reported through sceNetErrnoLoc (SCE_NET_ERROR_* is 0x80410100 + errno). +constexpr int NET_ENOENT = 2; +constexpr int NET_EBADF = 9; +constexpr int NET_EINVAL = 22; +constexpr int NET_EAGAIN = 35; +constexpr int NET_ENOTSOCK = 38; +constexpr int NET_EOPNOTSUPP = 45; +constexpr int NET_EPROTONOSUPPORT = 43; +constexpr int NET_EAFNOSUPPORT = 47; +constexpr int NET_EADDRINUSE = 48; +constexpr int NET_ENETUNREACH = 51; +constexpr int NET_ECONNABORTED = 53; +constexpr int NET_ENOTCONN = 57; +constexpr int NET_ETIMEDOUT = 60; +constexpr int NET_ECONNREFUSED = 61; +constexpr int NET_ERROR_RESOLVER_ENODNS = static_cast(0x804101E1u); -struct Epoll { - std::mutex lock; - std::condition_variable changed; +constexpr int NET_AF_INET = 2; +constexpr int NET_AF_INET6 = 28; +constexpr int NET_SOCK_STREAM = 1; +constexpr int NET_SOCK_DGRAM = 2; +constexpr int NET_SOCK_RAW = 3; +constexpr int NET_SOL_SOCKET = 0xFFFF; +constexpr int NET_SO_SNDTIMEO = 0x1005; +constexpr int NET_SO_RCVTIMEO = 0x1006; +constexpr int NET_SO_NBIO = 0x1200; + +struct Sock { + int family = 0; + int type = 0; + bool nonblock = false; + bool bound = false; + bool listening = false; bool aborted = false; + std::uint16_t port = 0; // network byte order + std::uint32_t addr = 0; // network byte order + int rcv_timeout_us = 0; + int snd_timeout_us = 0; }; -static std::mutex g_epollLock; -static std::map> g_epolls; +std::mutex g_mutex; +std::condition_variable g_cv; +std::map g_socks; +std::set g_epolls; +std::set g_pools; +std::set g_resolvers; +int g_next_sock = 32; +int g_next_epoll = 0x4000; +int g_next_pool = 1; +int g_next_resolver = 0x100; +std::uint16_t g_next_port = 49152; +bool g_net_inited = false; -static int Fail(int error) { - g_netErrno = error; - return static_cast(0x80410100u | static_cast(error)); +int* errno_slot() { + static thread_local int e = 0; + return &e; } -static std::shared_ptr FindEpoll(int eid) { - std::lock_guard lock(g_epollLock); - const auto found = g_epolls.find(eid); - return found == g_epolls.end() ? nullptr : found->second; +int fail(int err) { + *errno_slot() = err; + return -1; } -static void AbortEpoll(Epoll& epoll) { - { - std::lock_guard lock(epoll.lock); - epoll.aborted = true; +void log_soft(const char* func, const char* what) { + static std::mutex mtx; + static std::map hits; + std::lock_guard lk(mtx); + if (++hits[func] > 3) { + return; } - epoll.changed.notify_all(); + std::fprintf(stderr, "[SOFT-NET] %s -> %s\n", func, what); + std::fflush(stderr); } +std::uint16_t swap16(std::uint16_t v) { return static_cast((v << 8) | (v >> 8)); } +std::uint32_t swap32(std::uint32_t v) { + return (v << 24) | ((v & 0xFF00u) << 8) | ((v >> 8) & 0xFF00u) | (v >> 24); +} + +// Waits on the socket until the timeout expires or it is closed/aborted. +// timeout_us <= 0 nominally means "wait forever", but on an inert stack nothing can ever arrive, so such a +// wait is bounded to INFINITE_WAIT_SLICE_US per iteration and then reports EAGAIN: the caller keeps correct +// blocking-loop semantics without the process being able to deadlock on the network. +// Returns 0 on timeout, -1 with errno set when the socket went away or was aborted. +int block_on(std::unique_lock& lk, int fd, int timeout_us) { + constexpr int INFINITE_WAIT_SLICE_US = 100000; // 100 ms + const auto deadline = std::chrono::steady_clock::now() + + std::chrono::microseconds(timeout_us > 0 ? timeout_us : INFINITE_WAIT_SLICE_US); + for (;;) { + auto it = g_socks.find(fd); + if (it == g_socks.end()) { + return fail(NET_EBADF); + } + if (it->second.aborted) { + it->second.aborted = false; + return fail(NET_ECONNABORTED); + } + if (g_cv.wait_until(lk, deadline) == std::cv_status::timeout) { + return fail(NET_EAGAIN); + } + } +} +} // namespace + extern "C" { -int APS5_VABI sceNetAccept(int s, void* addr, uint32_t* addrlen) { - (void)s; - (void)addr; - (void)addrlen; - return Fail(GUEST_EBADF); -} - -int APS5_VABI sceNetBind_nid_postfix(int s, const void* addr, uint32_t addrlen) { - (void)s; - (void)addr; - (void)addrlen; - return Fail(GUEST_EBADF); -} - -int APS5_VABI sceNetEpollControl(int eid, int op, int id, const NetEpollEvent* event) { - (void)op; - (void)id; - (void)event; - if (!FindEpoll(eid)) return Fail(GUEST_EBADF); - return Fail(GUEST_EBADF); -} - -int APS5_VABI sceNetEpollCreate(const char* name, int flags) { - (void)name; - (void)flags; - const int id = g_nextId.fetch_add(1, std::memory_order_relaxed); - std::lock_guard lock(g_epollLock); - g_epolls[id] = std::make_shared(); - return id; -} - -int APS5_VABI sceNetEpollDestroy(int eid) { - std::lock_guard lock(g_epollLock); - const auto found = g_epolls.find(eid); - if (found == g_epolls.end()) return Fail(GUEST_EBADF); - AbortEpoll(*found->second); - g_epolls.erase(found); - return 0; -} - -int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int timeout) { - (void)events; - (void)maxevents; - auto epoll = FindEpoll(eid); - if (!epoll) return Fail(GUEST_EBADF); - std::unique_lock lock(epoll->lock); - const auto aborted = [&] { return epoll->aborted; }; - if (timeout < 0) epoll->changed.wait(lock, aborted); - else epoll->changed.wait_for(lock, std::chrono::microseconds(timeout), aborted); - if (epoll->aborted) { - epoll->aborted = false; - return Fail(GUEST_EINTR); - } - return 0; -} - -int APS5_VABI sceNetEtherNtostr(const NetEtherAddr* n, char* str, size_t len) { - (void)n; - (void)str; - (void)len; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNetGetMacAddress(NetEtherAddr* addr, int flags) { - (void)addr; - (void)flags; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNetGetSockInfo(int s, void* info, int n, int flags) { - (void)s; - (void)info; - (void)n; - (void)flags; - return Fail(GUEST_EBADF); -} - -int APS5_VABI sceNetGetsockname(int s, void* addr, uint32_t* addrlen) { - (void)s; - (void)addr; - (void)addrlen; - return Fail(GUEST_EBADF); -} - -uint32_t APS5_VABI sceNetHtonl_nid_postfix(uint32_t host32) { - return __builtin_bswap32(host32); -} - -uint16_t APS5_VABI sceNetHtons_nid_postfix(uint16_t host16) { - return __builtin_bswap16(host16); -} - -const char* APS5_VABI sceNetInetNtop(int af, const void* src, char* dst, uint32_t size) { - if (af != GUEST_AF_INET || !src || !dst) { - Fail(GUEST_EAFNOSUPPORT); - return nullptr; - } - const auto* bytes = static_cast(src); - if (std::snprintf(dst, size, "%u.%u.%u.%u", bytes[0], bytes[1], bytes[2], bytes[3]) >= static_cast(size)) { - Fail(GUEST_ENOSPC); - return nullptr; - } - return dst; -} - -int APS5_VABI sceNetInetPton(int af, const char* src, void* dst) { - if (af != GUEST_AF_INET) return Fail(GUEST_EAFNOSUPPORT); - if (!src || !dst) return Fail(GUEST_EINVAL); - unsigned parts[4]; - char trailing; - if (std::sscanf(src, "%u.%u.%u.%u%c", &parts[0], &parts[1], &parts[2], &parts[3], &trailing) != 4) return 0; - auto* bytes = static_cast(dst); - for (int index = 0; index < 4; ++index) { - if (parts[index] > 255) return 0; - bytes[index] = static_cast(parts[index]); - } - return 1; +int* APS5_VABI sceNetErrnoLoc(void) { + return errno_slot(); } int APS5_VABI sceNetInit_nid_postfix(void) { + std::lock_guard lk(g_mutex); + g_net_inited = true; return 0; } -int APS5_VABI sceNetListen(int s, int backlog) { - (void)s; - (void)backlog; - return Fail(GUEST_EBADF); -} - -uint32_t APS5_VABI sceNetNtohl_nid_postfix(uint32_t net32) { - return __builtin_bswap32(net32); -} - -uint16_t APS5_VABI sceNetNtohs_nid_postfix(uint16_t net16) { - return __builtin_bswap16(net16); +int APS5_VABI sceNetTerm(void) { + std::lock_guard lk(g_mutex); + g_net_inited = false; + return 0; } int APS5_VABI sceNetPoolCreate(const char* name, int size, int flags) { (void)name; (void)flags; - if (size <= 0) return Fail(GUEST_EINVAL); - return g_nextId.fetch_add(1, std::memory_order_relaxed); + if (size <= 0) { + return fail(NET_EINVAL); + } + std::lock_guard lk(g_mutex); + const int id = g_next_pool++; + g_pools.insert(id); + return id; } int APS5_VABI sceNetPoolDestroy(int memid) { - (void)memid; - return 0; -} - -int APS5_VABI sceNetResolverCreate(const char* name, int memid, int flags) { - (void)name; - (void)memid; - (void)flags; - return g_nextId.fetch_add(1, std::memory_order_relaxed); -} - -int APS5_VABI sceNetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout, int retry, int flags) { - (void)rid; - (void)hostname; - (void)addr; - (void)timeout; - (void)retry; - (void)flags; - return Fail(GUEST_EHOSTUNREACH); -} - -int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval, uint32_t optlen) { - (void)s; - (void)level; - (void)optname; - (void)optval; - (void)optlen; - return Fail(GUEST_EBADF); -} - -int APS5_VABI sceNetShutdown(int s, int how) { - (void)s; - (void)how; - return Fail(GUEST_EBADF); + std::lock_guard lk(g_mutex); + return g_pools.erase(memid) != 0 ? 0 : fail(NET_EBADF); } int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol) { (void)name; - (void)family; - (void)type; (void)protocol; - return Fail(GUEST_ENETDOWN); + if (family != NET_AF_INET && family != NET_AF_INET6) { + return fail(NET_EAFNOSUPPORT); + } + if (type != NET_SOCK_STREAM && type != NET_SOCK_DGRAM && type != NET_SOCK_RAW) { + return fail(NET_EPROTONOSUPPORT); + } + std::lock_guard lk(g_mutex); + const int fd = g_next_sock++; + Sock s; + s.family = family; + s.type = type; + g_socks[fd] = s; + return fd; } int APS5_VABI sceNetSocketClose(int s) { - (void)s; - return Fail(GUEST_EBADF); -} - -int APS5_VABI sceNetConnect(int s, const void* addr, uint32_t addrlen) { - (void)s; - (void)addr; - (void)addrlen; - return Fail(GUEST_EBADF); -} - -int APS5_VABI sceNetEpollAbort(int eid, int flags) { - (void)flags; - auto epoll = FindEpoll(eid); - if (!epoll) return Fail(GUEST_EBADF); - AbortEpoll(*epoll); + std::lock_guard lk(g_mutex); + if (g_socks.erase(s) == 0) { + return fail(NET_EBADF); + } + g_cv.notify_all(); return 0; } -int* APS5_VABI sceNetErrnoLoc(void) { - return &g_netErrno; +int APS5_VABI sceNetSocketAbort(int s, int flags) { + (void)flags; + std::lock_guard lk(g_mutex); + auto it = g_socks.find(s); + if (it == g_socks.end()) { + return fail(NET_EBADF); + } + it->second.aborted = true; + g_cv.notify_all(); + return 0; } -int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint32_t* optlen) { - (void)s; - (void)level; - (void)optname; - (void)optval; - (void)optlen; - return Fail(GUEST_EBADF); +int APS5_VABI sceNetBind_nid_postfix(int s, const void* addr, uint32_t addrlen) { + std::lock_guard lk(g_mutex); + auto it = g_socks.find(s); + if (it == g_socks.end()) { + return fail(NET_EBADF); + } + if (addr == nullptr || addrlen < 8) { + return fail(NET_EINVAL); + } + if (it->second.bound) { + return fail(NET_EINVAL); + } + // sockaddr_in: len, family, port (BE), addr (BE) + std::uint16_t port = 0; + std::uint32_t ip = 0; + std::memcpy(&port, static_cast(addr) + 2, 2); + std::memcpy(&ip, static_cast(addr) + 4, 4); + if (port == 0) { + port = swap16(g_next_port++); + } else { + for (const auto& kv : g_socks) { + if (kv.first != s && kv.second.bound && kv.second.port == port && kv.second.type == it->second.type) { + return fail(NET_EADDRINUSE); + } + } + } + it->second.bound = true; + it->second.port = port; + it->second.addr = ip; + return 0; } -int APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { - (void)s; +int APS5_VABI sceNetListen(int s, int backlog) { + (void)backlog; + std::lock_guard lk(g_mutex); + auto it = g_socks.find(s); + if (it == g_socks.end()) { + return fail(NET_EBADF); + } + if (it->second.type != NET_SOCK_STREAM) { + return fail(NET_EOPNOTSUPP); + } + if (!it->second.bound) { + it->second.bound = true; + it->second.port = swap16(g_next_port++); + } + it->second.listening = true; + return 0; +} + +int APS5_VABI sceNetAccept(int s, void* addr, uint32_t* addrlen) { + (void)addr; + (void)addrlen; + std::unique_lock lk(g_mutex); + auto it = g_socks.find(s); + if (it == g_socks.end()) { + return fail(NET_EBADF); + } + if (!it->second.listening) { + return fail(NET_EINVAL); + } + if (it->second.nonblock) { + return fail(NET_EAGAIN); + } + // No peer can ever arrive: wait for close/abort like an idle listener. + const int rc = block_on(lk, s, it->second.rcv_timeout_us); + return rc; +} + +int APS5_VABI sceNetConnect(int s, const void* addr, uint32_t addrlen) { + std::lock_guard lk(g_mutex); + auto it = g_socks.find(s); + if (it == g_socks.end()) { + return fail(NET_EBADF); + } + if (addr == nullptr || addrlen < 8) { + return fail(NET_EINVAL); + } + std::uint32_t ip = 0; + std::memcpy(&ip, static_cast(addr) + 4, 4); + if ((swap32(ip) >> 24) == 127) { + return fail(NET_ECONNREFUSED); // loopback with no listener + } + return fail(NET_ENETUNREACH); +} + +static int recv_common(int s) { + std::unique_lock lk(g_mutex); + auto it = g_socks.find(s); + if (it == g_socks.end()) { + return fail(NET_EBADF); + } + if (it->second.type == NET_SOCK_STREAM) { + return fail(NET_ENOTCONN); + } + if (it->second.nonblock) { + return fail(NET_EAGAIN); + } + return block_on(lk, s, it->second.rcv_timeout_us); +} + +int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { (void)buf; (void)len; (void)flags; - return Fail(GUEST_EBADF); + return recv_common(s); } -int APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* from, uint32_t* fromlen) { - (void)s; +int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* from, uint32_t* fromlen) { (void)buf; (void)len; (void)flags; (void)from; (void)fromlen; - return Fail(GUEST_EBADF); + return recv_common(s); } -int APS5_VABI sceNetResolverDestroy(int rid) { - (void)rid; +static int send_common(int s) { + std::lock_guard lk(g_mutex); + auto it = g_socks.find(s); + if (it == g_socks.end()) { + return fail(NET_EBADF); + } + return fail(it->second.type == NET_SOCK_STREAM ? NET_ENOTCONN : NET_ENETUNREACH); +} + +int64_t APS5_VABI sceNetSend(int s, const void* buf, size_t len, int flags) { + (void)buf; + (void)len; + (void)flags; + return send_common(s); +} + +int64_t APS5_VABI sceNetSendto(int s, const void* buf, size_t len, int flags, const void* to, uint32_t tolen) { + (void)buf; + (void)len; + (void)flags; + (void)to; + (void)tolen; + return send_common(s); +} + +int APS5_VABI sceNetShutdown(int s, int how) { + (void)how; + std::lock_guard lk(g_mutex); + auto it = g_socks.find(s); + if (it == g_socks.end()) { + return fail(NET_EBADF); + } + return it->second.type == NET_SOCK_STREAM ? fail(NET_ENOTCONN) : 0; +} + +int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval, uint32_t optlen) { + std::lock_guard lk(g_mutex); + auto it = g_socks.find(s); + if (it == g_socks.end()) { + return fail(NET_EBADF); + } + if (level == NET_SOL_SOCKET && optval != nullptr && optlen >= sizeof(int)) { + int v = 0; + std::memcpy(&v, optval, sizeof(v)); + switch (optname) { + case NET_SO_NBIO: it->second.nonblock = v != 0; break; + case NET_SO_RCVTIMEO: it->second.rcv_timeout_us = v; break; + case NET_SO_SNDTIMEO: it->second.snd_timeout_us = v; break; + default: break; + } + } return 0; } +int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint32_t* optlen) { + std::lock_guard lk(g_mutex); + auto it = g_socks.find(s); + if (it == g_socks.end()) { + return fail(NET_EBADF); + } + if (optval == nullptr || optlen == nullptr || *optlen < sizeof(int)) { + return fail(NET_EINVAL); + } + int v = 0; + if (level == NET_SOL_SOCKET) { + switch (optname) { + case NET_SO_NBIO: v = it->second.nonblock ? 1 : 0; break; + case NET_SO_RCVTIMEO: v = it->second.rcv_timeout_us; break; + case NET_SO_SNDTIMEO: v = it->second.snd_timeout_us; break; + default: break; + } + } + std::memcpy(optval, &v, sizeof(v)); + *optlen = sizeof(v); + return 0; +} + +int APS5_VABI sceNetGetsockname(int s, void* addr, uint32_t* addrlen) { + std::lock_guard lk(g_mutex); + auto it = g_socks.find(s); + if (it == g_socks.end()) { + return fail(NET_EBADF); + } + if (addr == nullptr || addrlen == nullptr) { + return fail(NET_EINVAL); + } + std::uint8_t sa[16] = {}; + sa[0] = 16; + sa[1] = static_cast(NET_AF_INET); + std::memcpy(sa + 2, &it->second.port, 2); + std::memcpy(sa + 4, &it->second.addr, 4); + const std::uint32_t n = *addrlen < sizeof(sa) ? *addrlen : static_cast(sizeof(sa)); + std::memcpy(addr, sa, n); + *addrlen = sizeof(sa); + return 0; +} + +int APS5_VABI sceNetGetSockInfo(int s, void* info, int n, int flags) { + (void)info; + (void)n; + (void)flags; + std::lock_guard lk(g_mutex); + return g_socks.count(s) != 0 ? 0 : fail(NET_EBADF); +} + +// Epoll: nothing can become ready without a network, so a wait sleeps for its timeout (microseconds; a +// negative infinite timeout is serviced in bounded slices) and reports zero events. +int APS5_VABI sceNetEpollCreate(const char* name, int flags) { + (void)name; + (void)flags; + std::lock_guard lk(g_mutex); + const int id = g_next_epoll++; + g_epolls.insert(id); + return id; +} + +int APS5_VABI sceNetEpollDestroy(int eid) { + std::lock_guard lk(g_mutex); + if (g_epolls.erase(eid) == 0) { + return fail(NET_EBADF); + } + g_cv.notify_all(); + return 0; +} + +int APS5_VABI sceNetEpollControl(int eid, int op, int id, const NetEpollEvent* event) { + (void)event; + std::lock_guard lk(g_mutex); + if (g_epolls.count(eid) == 0) { + return fail(NET_EBADF); + } + if (g_socks.count(id) == 0) { + return fail(NET_EBADF); + } + if (op < 1 || op > 3) { + return fail(NET_EINVAL); + } + return 0; +} + +int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int timeout) { + (void)events; + if (maxevents <= 0) { + return fail(NET_EINVAL); + } + std::unique_lock lk(g_mutex); + if (g_epolls.count(eid) == 0) { + return fail(NET_EBADF); + } + if (timeout == 0) { + return 0; + } + // timeout < 0 nominally waits until the epoll object is destroyed; bound each wait slice so an + // infinite-timeout waiter on the inert stack still returns periodically (0 events) instead of + // deadlocking if nobody ever destroys the epoll. + constexpr int INFINITE_WAIT_SLICE_US = 100000; // 100 ms + const auto deadline = std::chrono::steady_clock::now() + + std::chrono::microseconds(timeout > 0 ? timeout : INFINITE_WAIT_SLICE_US); + for (;;) { + if (g_cv.wait_until(lk, deadline) == std::cv_status::timeout) { + return 0; + } + if (g_epolls.count(eid) == 0) { + return fail(NET_EBADF); + } + } +} + +// Byte order and text conversion. +uint32_t APS5_VABI sceNetHtonl_nid_postfix(uint32_t host32) { return swap32(host32); } +uint16_t APS5_VABI sceNetHtons_nid_postfix(uint16_t host16) { return swap16(host16); } +uint32_t APS5_VABI sceNetNtohl_nid_postfix(uint32_t net32) { return swap32(net32); } +uint16_t APS5_VABI sceNetNtohs_nid_postfix(uint16_t net16) { return swap16(net16); } + +int APS5_VABI sceNetInetPton(int af, const char* src, void* dst) { + if (src == nullptr || dst == nullptr) { + return fail(NET_EINVAL); + } + if (af != NET_AF_INET) { + return fail(NET_EAFNOSUPPORT); + } + unsigned a = 0, b = 0, c = 0, d = 0; + char tail = 0; + if (std::sscanf(src, "%3u.%3u.%3u.%3u%c", &a, &b, &c, &d, &tail) != 4 || a > 255 || b > 255 || c > 255 || d > 255) { + return 0; + } + const std::uint8_t out[4] = {static_cast(a), static_cast(b), static_cast(c), static_cast(d)}; + std::memcpy(dst, out, 4); + return 1; +} + +const char* APS5_VABI sceNetInetNtop(int af, const void* src, char* dst, uint32_t size) { + if (src == nullptr || dst == nullptr || af != NET_AF_INET) { + *errno_slot() = af != NET_AF_INET ? NET_EAFNOSUPPORT : NET_EINVAL; + return nullptr; + } + const auto* p = static_cast(src); + char tmp[16]; + const int n = std::snprintf(tmp, sizeof(tmp), "%u.%u.%u.%u", p[0], p[1], p[2], p[3]); + if (n < 0 || static_cast(n) >= size) { + *errno_slot() = 28; // ENOSPC + return nullptr; + } + std::memcpy(dst, tmp, static_cast(n) + 1); + return dst; +} + +int APS5_VABI sceNetEtherNtostr(const NetEtherAddr* n, char* str, size_t len) { + if (n == nullptr || str == nullptr || len < 18) { + return fail(NET_EINVAL); + } + std::snprintf(str, len, "%02x:%02x:%02x:%02x:%02x:%02x", n->data[0], n->data[1], n->data[2], n->data[3], n->data[4], n->data[5]); + return 0; +} + +int APS5_VABI sceNetGetMacAddress(NetEtherAddr* addr, int flags) { + (void)flags; + if (addr == nullptr) { + return fail(NET_EINVAL); + } + // Locally administered address; there is no adapter to read one from. + const std::uint8_t mac[6] = {0x02, 0x50, 0x53, 0x35, 0x00, 0x01}; + std::memcpy(addr->data, mac, 6); + return 0; +} + +// Resolver: there is no DNS server to ask. +int APS5_VABI sceNetResolverCreate(const char* name, int memid, int flags) { + (void)name; + (void)memid; + (void)flags; + std::lock_guard lk(g_mutex); + const int id = g_next_resolver++; + g_resolvers.insert(id); + return id; +} + +int APS5_VABI sceNetResolverDestroy(int rid) { + std::lock_guard lk(g_mutex); + return g_resolvers.erase(rid) != 0 ? 0 : fail(NET_EBADF); +} + +int APS5_VABI sceNetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout, int retry, int flags) { + (void)hostname; + (void)addr; + (void)timeout; + (void)retry; + (void)flags; + std::lock_guard lk(g_mutex); + if (g_resolvers.count(rid) == 0) { + return fail(NET_EBADF); + } + log_soft(__func__, "ENODNS (no network)"); + *errno_slot() = NET_ETIMEDOUT; + return NET_ERROR_RESOLVER_ENODNS; +} + int APS5_VABI sceNetResolverStartAton(int rid, const void* addr, char* hostname, int len, int timeout, int retry, int flags) { - (void)rid; (void)addr; (void)hostname; (void)len; (void)timeout; (void)retry; (void)flags; - return Fail(GUEST_EHOSTUNREACH); + std::lock_guard lk(g_mutex); + if (g_resolvers.count(rid) == 0) { + return fail(NET_EBADF); + } + log_soft(__func__, "ENODNS (no network)"); + *errno_slot() = NET_ETIMEDOUT; + return NET_ERROR_RESOLVER_ENODNS; } - -int APS5_VABI sceNetSend(int s, const void* msg, size_t len, int flags) { - (void)s; - (void)msg; - (void)len; - (void)flags; - return Fail(GUEST_EBADF); -} - -int APS5_VABI sceNetSendto(int s, const void* msg, size_t len, int flags, const void* to, uint32_t tolen) { - (void)s; - (void)msg; - (void)len; - (void)flags; - (void)to; - (void)tolen; - return Fail(GUEST_EBADF); -} - -int APS5_VABI sceNetSocketAbort(int s, int flags) { - (void)s; - (void)flags; - return Fail(GUEST_EBADF); -} - } diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index a53683063..bb5821fd9 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -1,29 +1,173 @@ +#include #include +#include #include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +// Offline reliable-UDP: the library initialises and contexts can be created and bound, but no connection can +// ever be established because there is no network. + +namespace { +constexpr int RUDP_ERROR_NOT_INITIALIZED = static_cast(0x80770001u); +constexpr int RUDP_ERROR_ALREADY_INITIALIZED = static_cast(0x80770002u); +constexpr int RUDP_ERROR_INVALID_CONTEXT_ID = static_cast(0x80770003u); +constexpr int RUDP_ERROR_INVALID_ARGUMENT = static_cast(0x80770004u); +constexpr int RUDP_ERROR_CONN_RESET = static_cast(0x80770009u); +constexpr int RUDP_ERROR_CONN_REFUSED = static_cast(0x8077000Au); + +using GuestRudpEventHandler = void(APS5_VABI*)(int, int, int, void*); + +std::mutex g_mutex; +bool g_inited = false; +int g_next_ctx = 0; +std::map g_ctx; // id -> bound +GuestRudpEventHandler g_handler = nullptr; +void* g_handler_arg = nullptr; + +bool valid_ctx(int ctx) { + return g_ctx.count(ctx) != 0; +} +} // namespace + extern "C" { -int APS5_VABI sceRudpEnableInternalIOThread(uint32_t stack_size, uint32_t priority) { - (void)stack_size; - (void)priority; - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceRudpInit_nid_postfix(void* mem_pool, int mem_pool_size) { - (void)mem_pool; - (void)mem_pool_size; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)mem_pool; + (void)mem_pool_size; + std::lock_guard lk(g_mutex); + if (g_inited) { + return RUDP_ERROR_ALREADY_INITIALIZED; + } + g_inited = true; + return 0; } -int APS5_VABI sceRudpSetEventHandler(RudpEventHandler handler, void* arg) { - (void)handler; - (void)arg; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceRudpEnd(int ctx_id) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + return g_ctx.erase(ctx_id) != 0 ? 0 : RUDP_ERROR_INVALID_CONTEXT_ID; } +int APS5_VABI sceRudpTerminate(void) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + g_inited = false; + g_ctx.clear(); + return 0; +} + +int APS5_VABI sceRudpEnableInternalIOThread(uint32_t stack_size, uint32_t priority) { + (void)stack_size; + (void)priority; + std::lock_guard lk(g_mutex); + return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; +} + +int APS5_VABI sceRudpSetEventHandler(GuestRudpEventHandler handler, void* arg) { + std::lock_guard lk(g_mutex); + g_handler = handler; + g_handler_arg = arg; + return 0; +} + +int APS5_VABI sceRudpCreateContext(GuestRudpEventHandler handler, void* arg, int* ctx_id) { + (void)handler; + (void)arg; + if (ctx_id == nullptr) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + const int id = g_next_ctx++; + g_ctx[id] = false; + *ctx_id = id; + return 0; +} + +int APS5_VABI sceRudpBind(int ctx_id, uint16_t vport, int transport, uint16_t port) { + (void)vport; + (void)transport; + (void)port; + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (!valid_ctx(ctx_id)) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + g_ctx[ctx_id] = true; + return 0; +} + +int APS5_VABI sceRudpSetOption(int ctx_id, int option, const void* value, uint32_t len) { + (void)option; + (void)value; + (void)len; + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + return valid_ctx(ctx_id) ? 0 : RUDP_ERROR_INVALID_CONTEXT_ID; +} + +int APS5_VABI sceRudpInitiate(int ctx_id, const void* to, uint32_t tolen, uint16_t vport) { + (void)to; + (void)tolen; + (void)vport; + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (!valid_ctx(ctx_id)) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + return RUDP_ERROR_CONN_REFUSED; +} + +int APS5_VABI sceRudpGetContextStatus(int ctx_id, void* status, uint32_t size) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (!valid_ctx(ctx_id)) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + if (status != nullptr && size != 0) { + std::memset(status, 0, size); // state 0 = idle, nothing pending + } + return 0; +} + +int APS5_VABI sceRudpWrite(int ctx_id, const void* data, uint32_t len, int flags) { + (void)data; + (void)len; + (void)flags; + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + return valid_ctx(ctx_id) ? RUDP_ERROR_CONN_RESET : RUDP_ERROR_INVALID_CONTEXT_ID; +} + +int APS5_VABI sceRudpRead(int ctx_id, void* data, uint32_t len, int flags, void* status) { + (void)data; + (void)len; + (void)flags; + (void)status; + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + return valid_ctx(ctx_id) ? RUDP_ERROR_CONN_RESET : RUDP_ERROR_INVALID_CONTEXT_ID; +} } From 77ed5e6d0c2cbc24eebd83cb41a84a8a8f30aa46 Mon Sep 17 00:00:00 2001 From: OverkillLabs <10800190+OverkillLabs@users.noreply.github.com> Date: Mon, 28 Sep 2026 12:48:09 +0300 Subject: [PATCH 12/46] feat(savedata): mount aliasing, wildcard search, memory slots, async events Rework the local SaveData implementation into a general-purpose, title-agnostic backend used by both the native library and the standard libSceSaveData path: - Directory aliasing: long save directory names are hashed into short mount point aliases (persisted to disk) so they fit the SDK's fixed-size mount point buffer, with the aliases reversed on directory search. - Wildcard directory-name search (%/_ patterns) with sorting and optional reverse order, matching the DirNameSearch contract. - Save-data "memory" slots: small per-user, per-slot blobs the title can set up, read and write without mounting a directory, backed by files under the save root with a companion parameter blob, size and range validation on every access, and a dirty flag consulted on commit. - Async-flavored operations (backup, commit, memory sync) that queue SaveDataEvent records consumed through GetEventResult and can be bound to kernel event flags, plus transaction-resource and cancel/abort bookkeeping. - Atomic file writes (temp file + rename) so a mid-write crash never leaves a torn save on disk, and corrected/duplicated SDK error code constants. - Local storage app-status get/set/delete and transferring-mount support for PS4-to-PS5 style save transfers. The native library keeps its own lighter-weight mount/memory implementation (short "/_sm/" mount point aliases via the path-alias table) while gaining the same per-user memory-slot separation, atomic writes and bounds checking; the two implementations were merged by hand where upstream had independently touched the native Export.cpp in the same area (backup/event queue, short mount points) to keep both sets of improvements. None of this is game-specific: it is a general local save-data backend that works for any title and tolerates any user id. --- .../libs/prx/libSceSaveData.native/Export.cpp | 254 +++- .../prx/libSceSaveData.native/SaveData.hpp | 17 +- core/libs/prx/libSceSaveData/Export.cpp | 107 +- core/libs/prx/libSceSaveData/SaveDataCore.hpp | 1354 +++++++++++++++++ core/libs/prx/libSceSaveDataDialog/Export.cpp | 46 +- 5 files changed, 1658 insertions(+), 120 deletions(-) create mode 100644 core/libs/prx/libSceSaveData/SaveDataCore.hpp diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index ce6f82d14..6e1a7d4cc 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -24,24 +24,11 @@ static std::string save_root() { return std::string(SAVE_DIR); } -// Events the title polls with sceSaveDataGetEventResult (a backup completes at once here), and the -// "save data memory" blobs (small per-slot memories the title reads and writes without mounting), -// persisted as files under the save root so they survive restarts. +// Events the title polls with sceSaveDataGetEventResult; a backup completes at once here. static std::mutex g_state_mutex; static std::deque g_events; -static std::map> g_memories; constexpr std::uint32_t SAVE_DATA_EVENT_TYPE_BACKUP = 2; -static std::string memory_path(std::uint32_t slot) { - return save_root() + "/_memory_" + std::to_string(slot) + ".bin"; -} - -static void store_memory(std::uint32_t slot, const std::vector& memory) { - std::filesystem::create_directories(save_root()); - std::ofstream file(memory_path(slot), std::ios::binary | std::ios::trunc); - file.write(reinterpret_cast(memory.data()), static_cast(memory.size())); -} - static bool dir_name_match(const char* str, const char* pattern) { if (pattern == nullptr || pattern[0] == '\0') { return true; @@ -72,6 +59,78 @@ static bool dir_name_match(const char* str, const char* pattern) { return *str == '\0' && *pattern == '\0'; } +namespace { + +// Save-data memory: one blob per user and slot, kept next to the save directory (_sd_mem/u/slot.bin), +// with a small .param sidecar holding the last SaveDataParam the title wrote. Keying by user as well as slot +// keeps two users' in-memory saves from colliding on the same console. +constexpr char MEM_DIR[] = "_sd_mem"; +constexpr std::size_t MEM_MAX_SIZE = 0x1000000; // 16 MiB +std::mutex g_mem_mutex; + +std::string mem_path(std::int32_t user_id, std::uint32_t slot, const char* ext) { + char buf[96]; + std::snprintf(buf, sizeof(buf), "%s/u%d/slot%u.%s", MEM_DIR, static_cast(user_id), static_cast(slot), ext); + return buf; +} + +bool file_size_of(const std::string& path, std::size_t* out) { + std::error_code ec; + if (!std::filesystem::is_regular_file(path, ec)) { + return false; + } + const auto sz = std::filesystem::file_size(path, ec); + if (ec) { + return false; + } + *out = static_cast(sz); + return true; +} + +// Atomic-ish write: write to a temp file then rename over the target, so a kill mid-write never +// leaves a torn save behind. +bool write_file_replace(const std::string& path, const std::vector& data) { + const std::string tmp = path + ".tmp"; + { + std::ofstream f(tmp, std::ios::binary | std::ios::trunc); + if (!f) { + return false; + } + if (!data.empty()) { + f.write(data.data(), static_cast(data.size())); + } + f.flush(); + if (!f) { + return false; + } + } + std::error_code ec; + std::filesystem::rename(tmp, path, ec); + if (ec) { + std::filesystem::remove(path, ec); + ec.clear(); + std::filesystem::rename(tmp, path, ec); + } + return !ec; +} + +bool read_file_all(const std::string& path, std::vector& out) { + std::ifstream f(path, std::ios::binary); + if (!f) { + return false; + } + f.seekg(0, std::ios::end); + const auto n = static_cast(f.tellg()); + f.seekg(0); + out.resize(n); + if (n != 0) { + f.read(out.data(), static_cast(n)); + } + return static_cast(f); +} + +} // namespace + extern "C" { int APS5_VABI sceSaveDataBackup(const SaveDataBackup* backup) { @@ -196,29 +255,47 @@ int APS5_VABI sceSaveDataGetParam(const SaveDataMountPoint* mount_point, uint32_ int APS5_VABI sceSaveDataGetSaveDataMemory2(SaveDataMemoryGet2* get_param) { if (get_param == nullptr) { - throw std::runtime_error("sceSaveDataGetSaveDataMemory2: null argument"); + return SAVE_DATA_ERROR_PARAMETER; } - std::lock_guard lock(g_state_mutex); - const auto it = g_memories.find(get_param->slot_id); - if (it == g_memories.end()) { - return SAVE_DATA_ERROR_NOT_FOUND; + if (!g_initialized) { + return SAVE_DATA_ERROR_NOT_INITIALIZED; } - if (get_param->data != nullptr && get_param->data->buf != nullptr) { - const auto& memory = it->second; - if (get_param->data->offset > memory.size()) { + std::lock_guard lk(g_mem_mutex); + const std::string path = mem_path(get_param->user_id, get_param->slot_id, "bin"); + std::size_t size = 0; + if (!file_size_of(path, &size)) { + return SAVE_DATA_ERROR_MEMORY_NOT_READY; + } + const SaveDataMemoryData* d = get_param->data; + if (d != nullptr && d->buf_size != 0) { + if (d->buf == nullptr || d->offset > size || d->buf_size > size - d->offset) { return SAVE_DATA_ERROR_PARAMETER; } - const auto bytes = std::min(get_param->data->buf_size, memory.size() - get_param->data->offset); - std::memcpy(get_param->data->buf, memory.data() + get_param->data->offset, bytes); + std::ifstream f(path, std::ios::binary); + if (!f) { + return SAVE_DATA_ERROR_INTERNAL; + } + f.seekg(static_cast(d->offset)); + f.read(static_cast(d->buf), static_cast(d->buf_size)); + if (!f) { + return SAVE_DATA_ERROR_INTERNAL; + } + } + if (get_param->param != nullptr) { + std::memset(get_param->param, 0, sizeof(SaveDataParam)); + std::vector pd; + if (read_file_all(mem_path(get_param->user_id, get_param->slot_id, "param"), pd)) { + std::memcpy(get_param->param, pd.data(), std::min(pd.size(), sizeof(SaveDataParam))); + } + } + if (get_param->icon != nullptr) { + get_param->icon->data_size = 0; } - if (get_param->param != nullptr) std::memset(get_param->param, 0, sizeof(SaveDataParam)); - if (get_param->icon != nullptr) get_param->icon->data_size = 0; return SAVE_DATA_OK; } int APS5_VABI sceSaveDataInitialize3(const void* init) { (void)init; - // NotImplemented_nid_no_patch(__func__); if (g_initialized) { return SAVE_DATA_ERROR_ALREADY_INITIALIZED; } @@ -332,59 +409,108 @@ int APS5_VABI sceSaveDataSetParam(const SaveDataMountPoint* mount_point, uint32_ int APS5_VABI sceSaveDataSetSaveDataMemory2(const SaveDataMemorySet2* set_param) { if (set_param == nullptr) { - throw std::runtime_error("sceSaveDataSetSaveDataMemory2: null argument"); + return SAVE_DATA_ERROR_PARAMETER; } - std::lock_guard lock(g_state_mutex); - const auto it = g_memories.find(set_param->slot_id); - if (it == g_memories.end()) { - return SAVE_DATA_ERROR_NOT_FOUND; + if (!g_initialized) { + return SAVE_DATA_ERROR_NOT_INITIALIZED; } - auto& memory = it->second; - if (set_param->data != nullptr) { - const auto count = set_param->data_num == 0 ? 1u : set_param->data_num; - for (std::uint32_t i = 0; i < count; ++i) { - const auto& data = set_param->data[i]; - if (data.buf == nullptr) continue; - if (data.offset > memory.size() || data.buf_size > memory.size() - data.offset) { - return SAVE_DATA_ERROR_PARAMETER; - } - std::memcpy(memory.data() + data.offset, data.buf, data.buf_size); + std::lock_guard lk(g_mem_mutex); + const std::string path = mem_path(set_param->user_id, set_param->slot_id, "bin"); + std::size_t size = 0; + if (!file_size_of(path, &size)) { + return SAVE_DATA_ERROR_MEMORY_NOT_READY; + } + // Validate every range first so a bad entry never leaves a partial write. + const std::uint32_t n = set_param->data != nullptr ? (set_param->data_num != 0 ? set_param->data_num : 1u) : 0u; + for (std::uint32_t i = 0; i < n; i++) { + const SaveDataMemoryData& d = set_param->data[i]; + if (d.buf_size == 0) { + continue; + } + if (d.buf == nullptr || d.offset > size || d.buf_size > size - d.offset) { + return SAVE_DATA_ERROR_PARAMETER; } } - store_memory(set_param->slot_id, memory); + if (n != 0) { + std::fstream f(path, std::ios::binary | std::ios::in | std::ios::out); + if (!f) { + return SAVE_DATA_ERROR_INTERNAL; + } + for (std::uint32_t i = 0; i < n; i++) { + const SaveDataMemoryData& d = set_param->data[i]; + if (d.buf_size == 0) { + continue; + } + f.seekp(static_cast(d.offset)); + f.write(static_cast(d.buf), static_cast(d.buf_size)); + } + f.flush(); + if (!f) { + return SAVE_DATA_ERROR_INTERNAL; + } + } + if (set_param->param != nullptr) { + std::vector pd(sizeof(SaveDataParam)); + std::memcpy(pd.data(), set_param->param, sizeof(SaveDataParam)); + write_file_replace(mem_path(set_param->user_id, set_param->slot_id, "param"), pd); + } return SAVE_DATA_OK; } int APS5_VABI sceSaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, SaveDataMemorySetupResult* result) { if (setup_param == nullptr) { - throw std::runtime_error("sceSaveDataSetupSaveDataMemory2: null argument"); + return SAVE_DATA_ERROR_PARAMETER; } - std::lock_guard lock(g_state_mutex); - auto& memory = g_memories[setup_param->slot_id]; + if (!g_initialized) { + return SAVE_DATA_ERROR_NOT_INITIALIZED; + } + if (setup_param->memory_size == 0 || setup_param->memory_size > MEM_MAX_SIZE) { + return SAVE_DATA_ERROR_PARAMETER; + } + std::lock_guard lk(g_mem_mutex); + const std::string path = mem_path(setup_param->user_id, setup_param->slot_id, "bin"); std::size_t existed = 0; - if (memory.empty()) { - std::ifstream file(memory_path(setup_param->slot_id), std::ios::binary); - if (file) { - memory.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); - existed = memory.size(); + const bool have = file_size_of(path, &existed); + if (!have) { + existed = 0; + } + // First run: create a zero-filled blob and report existed size 0 so the title treats it as a new save. + if (!have || existed < setup_param->memory_size) { + std::error_code ec; + std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec); + std::vector data; + if (have) { + read_file_all(path, data); + } + data.resize(setup_param->memory_size, 0); + if (!write_file_replace(path, data)) { + return SAVE_DATA_ERROR_INTERNAL; + } + if (setup_param->init_param != nullptr && (setup_param->option & 1u) != 0) { + std::vector pd(sizeof(SaveDataParam)); + std::memcpy(pd.data(), setup_param->init_param, sizeof(SaveDataParam)); + write_file_replace(mem_path(setup_param->user_id, setup_param->slot_id, "param"), pd); } - } else { - existed = memory.size(); } - if (memory.size() < setup_param->memory_size) { - memory.resize(setup_param->memory_size, 0); - } - if (existed == 0) store_memory(setup_param->slot_id, memory); if (result != nullptr) { std::memset(result, 0, sizeof(*result)); - result->existed_memory_size = existed; + result->existed_memory_size = have ? existed : 0; } return SAVE_DATA_OK; } int APS5_VABI sceSaveDataSyncSaveDataMemory(const void* sync_param) { - (void)sync_param; - // Every Set already reached the file; nothing is buffered. + if (sync_param == nullptr) { + return SAVE_DATA_ERROR_PARAMETER; + } + // Every Set already writes straight through to disk; sync only has to confirm the memory exists. + const std::int32_t user_id = *static_cast(sync_param); + const std::uint32_t slot_id = reinterpret_cast(sync_param)[1]; + std::lock_guard lk(g_mem_mutex); + std::size_t size = 0; + if (!file_size_of(mem_path(user_id, slot_id, "bin"), &size)) { + return SAVE_DATA_ERROR_MEMORY_NOT_READY; + } return SAVE_DATA_OK; } @@ -401,9 +527,11 @@ int APS5_VABI sceSaveDataTerminate(void) { int APS5_VABI sceSaveDataTransferringMount(const SaveDataTransferringMount* mount, SaveDataMountResult* mount_result) { (void)mount; - (void)mount_result; - NotImplemented_nid_no_patch(__func__); - return 0; + if (mount_result != nullptr) { + std::memset(mount_result, 0, sizeof(*mount_result)); + } + // No PS4-to-PS5 transfer data exists on this console. + return SAVE_DATA_ERROR_NOT_FOUND; } int APS5_VABI sceSaveDataUmount2(uint32_t mode, const SaveDataMountPoint* mount_point) { diff --git a/core/libs/prx/libSceSaveData.native/SaveData.hpp b/core/libs/prx/libSceSaveData.native/SaveData.hpp index dd69aca6c..95cf838c1 100644 --- a/core/libs/prx/libSceSaveData.native/SaveData.hpp +++ b/core/libs/prx/libSceSaveData.native/SaveData.hpp @@ -6,15 +6,18 @@ #include constexpr int SAVE_DATA_OK = 0; +// Values follow the SDK error table (0x809F0000 + code): 2 OUT_OF_MEMORY, 3 BUSY, 4 NOT_MOUNTED, 7 EXISTS, 8 NOT_FOUND, 0xB INTERNAL, 0xC MOUNT_FULL. constexpr int SAVE_DATA_ERROR_PARAMETER = -2137063424; constexpr int SAVE_DATA_ERROR_NOT_INITIALIZED = -2137063423; -constexpr int SAVE_DATA_ERROR_ALREADY_INITIALIZED = -2137063422; -constexpr int SAVE_DATA_ERROR_OUT_OF_MEMORY = -2137063421; -constexpr int SAVE_DATA_ERROR_BUSY = -2137063420; -constexpr int SAVE_DATA_ERROR_NOT_MOUNTED = -2137063419; -constexpr int SAVE_DATA_ERROR_MOUNT_FULL = -2137063418; -constexpr int SAVE_DATA_ERROR_EXISTS = -2137063414; -constexpr int SAVE_DATA_ERROR_NOT_FOUND = -2137063413; +constexpr int SAVE_DATA_ERROR_ALREADY_INITIALIZED = -2137063419; // 0x809F0005 +constexpr int SAVE_DATA_ERROR_OUT_OF_MEMORY = -2137063422; +constexpr int SAVE_DATA_ERROR_BUSY = -2137063421; +constexpr int SAVE_DATA_ERROR_NOT_MOUNTED = -2137063420; +constexpr int SAVE_DATA_ERROR_MOUNT_FULL = -2137063412; +constexpr int SAVE_DATA_ERROR_EXISTS = -2137063417; +constexpr int SAVE_DATA_ERROR_NOT_FOUND = -2137063416; +constexpr int SAVE_DATA_ERROR_INTERNAL = -2137063413; // 0x809F000B +constexpr int SAVE_DATA_ERROR_MEMORY_NOT_READY = -2137063406; // 0x809F0012 constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_RDONLY = 1; constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_RDWR = 2; diff --git a/core/libs/prx/libSceSaveData/Export.cpp b/core/libs/prx/libSceSaveData/Export.cpp index 0bc709e26..0d44da8d1 100644 --- a/core/libs/prx/libSceSaveData/Export.cpp +++ b/core/libs/prx/libSceSaveData/Export.cpp @@ -1,53 +1,94 @@ +#include #include +#include #include #include "SceTypes.hpp" +#include "prx/libSceSaveData/SaveDataCore.hpp" #include "prx/libc/include/General.hpp" +// Savedata imports bind by NID either to libSceSaveData.native or (via the relinker alias table, +// by name) to this library. Every entry point used to return SoftFailNp (0x800242e2), which is the +// NP-family "online service not available" code - wrong dialect for a purely local save call and +// fatal-looking for callers. All entry points now delegate to SaveDataCore.hpp: a local, +// file-backed implementation that needs no PSN account and tolerates any userId +// (0 / 65535 / 268435456), mirroring the native library. + +namespace { +// Logs the first three calls of each entry point, then stays silent, keeping guest logs readable. +#define SAVEDATA_TRACE(...) \ + do { \ + static std::atomic traceCount{0}; \ + if (traceCount.fetch_add(1, std::memory_order_relaxed) < 3) { \ + std::fprintf(stderr, "[SAVEDATA:core] " __VA_ARGS__); \ + std::fputc('\n', stderr); \ + std::fflush(stderr); \ + } \ + } while (0) +} // namespace + extern "C" { +int APS5_VABI sceSaveDataInitialize3(const void* init) { + const int rc = savedata::initialize_internal(init); + SAVEDATA_TRACE("sceSaveDataInitialize3 -> 0x%08x", static_cast(rc)); + return rc; +} +int APS5_VABI sceSaveDataTerminate(void) { + const int rc = savedata::terminate_internal(); + SAVEDATA_TRACE("sceSaveDataTerminate -> 0x%08x", static_cast(rc)); + return rc; +} int APS5_VABI sceSaveDataBackup(const SaveDataBackup* backup) { - (void)backup; - NotImplemented_nid_no_patch(__func__); - return 0; + const int rc = savedata::backup_internal(backup, nullptr); + SAVEDATA_TRACE("sceSaveDataBackup -> 0x%08x", static_cast(rc)); + return rc; } - int APS5_VABI sceSaveDataGetEventResult(const void* event_param, SaveDataEvent* event) { - (void)event_param; - (void)event; - NotImplemented_nid_no_patch(__func__); - return 0; + const int rc = savedata::get_event_result_internal(event_param, event); + SAVEDATA_TRACE("sceSaveDataGetEventResult -> 0x%08x", static_cast(rc)); + return rc; } - int APS5_VABI sceSaveDataGetSaveDataMemory2(SaveDataMemoryGet2* get_param) { - (void)get_param; - NotImplemented_nid_no_patch(__func__); - return 0; + if (get_param == nullptr) { + return savedata::SD_ERROR_PARAMETER; + } + const int rc = savedata::get_memory_internal(get_param); + SAVEDATA_TRACE("get2 user=%d slot=%u -> 0x%08x", static_cast(get_param->user_id), + static_cast(get_param->slot_id), static_cast(rc)); + return rc; } - int APS5_VABI sceSaveDataSetSaveDataMemory2(const SaveDataMemorySet2* set_param) { - (void)set_param; - NotImplemented_nid_no_patch(__func__); - return 0; + if (set_param == nullptr) { + return savedata::SD_ERROR_PARAMETER; + } + const int rc = savedata::set_memory_internal(set_param); + SAVEDATA_TRACE("set2 user=%d slot=%u num=%u -> 0x%08x", + static_cast(set_param->user_id), static_cast(set_param->slot_id), + static_cast(set_param->data_num), static_cast(rc)); + return rc; } - -int APS5_VABI sceSaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, SaveDataMemorySetupResult* result) { - (void)setup_param; - (void)result; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, + SaveDataMemorySetupResult* result) { + const int rc = savedata::setup_memory_internal(setup_param, result, nullptr); + SAVEDATA_TRACE("setup2 -> 0x%08x existed=0x%zx", static_cast(rc), + result != nullptr ? result->existed_memory_size : static_cast(0)); + return rc; } - int APS5_VABI sceSaveDataSyncSaveDataMemory(const void* sync_param) { - (void)sync_param; - NotImplemented_nid_no_patch(__func__); - return 0; + if (sync_param == nullptr) { + return savedata::SD_ERROR_PARAMETER; + } + const auto* sync = static_cast(sync_param); + const int rc = savedata::sync_memory_internal(sync->user_id, sync->slot_id, sync->option, nullptr, true); + SAVEDATA_TRACE("sync user=%d slot=%u option=%u -> 0x%08x", static_cast(sync->user_id), + static_cast(sync->slot_id), static_cast(sync->option), + static_cast(rc)); + return rc; } - -int APS5_VABI sceSaveDataTransferringMount(const SaveDataTransferringMount* mount, SaveDataMountResult* mount_result) { - (void)mount; - (void)mount_result; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSaveDataTransferringMount(const SaveDataTransferringMount* mount, + SaveDataMountResult* mount_result) { + const int rc = savedata::transferring_mount_internal(mount, mount_result); + SAVEDATA_TRACE("sceSaveDataTransferringMount -> 0x%08x", static_cast(rc)); + return rc; } - } diff --git a/core/libs/prx/libSceSaveData/SaveDataCore.hpp b/core/libs/prx/libSceSaveData/SaveDataCore.hpp new file mode 100644 index 000000000..166176bfe --- /dev/null +++ b/core/libs/prx/libSceSaveData/SaveDataCore.hpp @@ -0,0 +1,1354 @@ +#ifndef CORE_LIBS_PRX_LIBSCESAVEDATA_SAVEDATACORE_HPP +#define CORE_LIBS_PRX_LIBSCESAVEDATA_SAVEDATACORE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace savedata { + +extern "C" { +int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, std::uint32_t attr, + std::uint64_t init_pattern, const void* param); +int APS5_VABI sceKernelDeleteEventFlag(KernelEventFlag ef); +int APS5_VABI sceKernelSetEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern); +int APS5_VABI sceKernelCancelEventFlag(KernelEventFlag ef, std::uint64_t set_pattern, int* num_wait_threads); +} + +namespace { + +constexpr std::uint32_t SD_FACILITY = 0x809F0000u; +constexpr int sd_error(std::uint32_t code) { return static_cast(SD_FACILITY + code); } + +constexpr int SD_OK = 0; +constexpr int SD_ERROR_PARAMETER = sd_error(0x00); +constexpr int SD_ERROR_NOT_INITIALIZED = sd_error(0x01); +constexpr int SD_ERROR_OUT_OF_MEMORY = sd_error(0x02); +constexpr int SD_ERROR_BUSY = sd_error(0x03); +constexpr int SD_ERROR_NOT_MOUNTED = sd_error(0x04); +constexpr int SD_ERROR_NO_PERMISSION = sd_error(0x05); +constexpr int SD_ERROR_FINGERPRINT_MISMATCH = sd_error(0x06); +constexpr int SD_ERROR_EXISTS = sd_error(0x07); +constexpr int SD_ERROR_NOT_FOUND = sd_error(0x08); +constexpr int SD_ERROR_NO_SPACE_FS = sd_error(0x0A); +constexpr int SD_ERROR_INTERNAL = sd_error(0x0B); +constexpr int SD_ERROR_MOUNT_FULL = sd_error(0x0C); +constexpr int SD_ERROR_BAD_MOUNTED = sd_error(0x0D); +constexpr int SD_ERROR_FILE_NOT_FOUND = sd_error(0x0E); +constexpr int SD_ERROR_BROKEN = sd_error(0x0F); +constexpr int SD_ERROR_INVALID_LOGIN_USER = sd_error(0x11); +constexpr int SD_ERROR_MEMORY_NOT_READY = sd_error(0x12); +constexpr int SD_ERROR_BACKUP_BUSY = sd_error(0x13); +constexpr int SD_ERROR_NOT_REGIST_CALLBACK = sd_error(0x15); +constexpr int SD_ERROR_BUSY_FOR_SAVING = sd_error(0x16); +constexpr int SD_ERROR_LIMITATION_OVER = sd_error(0x17); +constexpr int SD_ERROR_EVENT_BUSY = sd_error(0x18); + +constexpr std::uint32_t SD_MOUNT_RDONLY = 1u; +constexpr std::uint32_t SD_MOUNT_RDWR = 2u; +constexpr std::uint32_t SD_MOUNT_CREATE = 4u; +constexpr std::uint32_t SD_MOUNT_COPY_ICON = 16u; +constexpr std::uint32_t SD_MOUNT_CREATE2 = 32u; + +constexpr std::uint32_t SD_UMOUNT_BACKUP_ASYNC = 1u << 16u; +constexpr std::uint32_t SD_COMMIT_BACKUP_ASYNC = 1u; + +constexpr std::uint32_t SD_EVENT_UMOUNT_BACKUP_END = 1u; +constexpr std::uint32_t SD_EVENT_BACKUP_END = 2u; +constexpr std::uint32_t SD_EVENT_MEMORY_SYNC_END = 3u; +constexpr std::uint32_t SD_EVENT_COMMIT_BACKUP_END = 4u; + +constexpr std::uint64_t SD_BLOCKS_MIN = 1; +constexpr std::uint64_t SD_BLOCKS_MAX = 32768; + +constexpr std::uint32_t SD_MEMORY_SLOTS = 4; +constexpr std::uint32_t SD_MEMORY_MAX_MOUNTED = 4; +constexpr std::size_t SD_MEMORY_MAX_SIZE = 32u * 1024u * 1024u; +constexpr std::uint32_t SD_MEMORY_SET_PARAM = 1u; +constexpr std::uint32_t SD_MEMORY_DOUBLE_BUFFER = 2u; +constexpr std::uint32_t SD_MEMORY_MAX_DATA_NUM = 5; + +constexpr std::uint32_t SD_PARAM_WHOLE = 0; +constexpr std::uint32_t SD_PARAM_TITLE = 1; +constexpr std::uint32_t SD_PARAM_SUB_TITLE = 2; +constexpr std::uint32_t SD_PARAM_DETAIL = 3; +constexpr std::uint32_t SD_PARAM_USER_PARAM = 4; +constexpr std::uint32_t SD_PARAM_MTIME = 5; + +constexpr std::uint32_t SD_APP_STATUS_NONE = 0; +constexpr std::uint32_t SD_APP_STATUS_DELETED = 1; +constexpr std::uint32_t SD_APP_STATUS_LOCAL = 2; +constexpr std::uint32_t SD_APP_STATUS_ONLINE = 4; +constexpr std::uint32_t SD_APP_STATUS_ANY = 0x7FFFFFFFu; + +constexpr char SD_ROOT_GUEST[] = "/_sd"; +constexpr char SD_SUBDIR[] = "sce_sys"; +constexpr char SD_MEMORY_PREFIX[] = "sce_sdmemory"; +constexpr std::size_t SD_MOUNT_PREFIX_LEN = sizeof(SD_ROOT_GUEST); +constexpr std::size_t SD_MOUNT_POINT_CAPACITY = sizeof(SaveDataMountPoint) - SD_MOUNT_PREFIX_LEN - 1; + +struct MemoryData { + void* buf; + std::size_t buf_size; + std::int64_t offset; + std::uint8_t reserved[40]; +}; +static_assert(sizeof(MemoryData) == 64, "SceSaveDataMemoryData is 64 bytes"); + +struct MemorySync { + std::int32_t user_id; + std::uint32_t slot_id; + std::uint32_t option; + std::uint8_t reserved[28]; +}; +static_assert(sizeof(MemorySync) == 40, "SceSaveDataMemorySync is 40 bytes"); + +struct EventParam { + std::size_t size; + std::uint8_t reserved[24]; +}; + +struct MountSlot { + bool used = false; + std::string mount_point; + std::string dir_name; + std::filesystem::path directory; +}; + +struct MemorySlot { + std::vector data; + SaveDataParam param {}; + std::uint32_t option = 0; + bool dirty = false; +}; + +struct PendingEvent { + SaveDataEvent record {}; + KernelEventFlag flag = nullptr; +}; + +struct State { + std::mutex mutex; + std::uint32_t instances = 0; + std::array slots {}; + std::deque events; + std::unordered_map memory; + std::map aliases; + std::int32_t next_transaction_resource = 1; + std::set live_transaction_resources; + void* event_callback = nullptr; + void* event_callback_userdata = nullptr; + bool aliases_loaded = false; +}; + +State& state() { + static State instance; + return instance; +} + +bool is_ready() { + return state().instances != 0; +} + +bool valid_dir_name(const char* name) { + if (name == nullptr || name[0] == '\0') { + return false; + } + std::size_t length = 0; + while (name[length] != '\0') { + length++; + if (length >= sizeof(SceSaveDataDirName::data)) { + return false; + } + } + if (std::strcmp(name, ".") == 0 || std::strcmp(name, "..") == 0) { + return false; + } + for (const char* p = name; *p != '\0'; p++) { + const bool ok = std::isalnum(static_cast(*p)) != 0 || *p == '_' || *p == '-' || + *p == '.' || *p == '@'; + if (!ok) { + return false; + } + } + return true; +} + +bool bounded_string_ok(const char* data, std::size_t capacity) { + return std::memchr(data, '\0', capacity) != nullptr; +} + +const std::filesystem::path& save_root() { + static const std::filesystem::path root = ResolvePath_nid_no_patch(SD_ROOT_GUEST); + return root; +} + +bool ensure_save_root(std::error_code& error) { + std::filesystem::create_directories(save_root(), error); + if (error) { + error.clear(); + } + return std::filesystem::is_directory(save_root(), error); +} + +void load_aliases_locked(State& st) { + if (st.aliases_loaded) { + return; + } + st.aliases_loaded = true; + std::ifstream file(save_root() / "_aliases", std::ios::binary); + std::string line; + while (std::getline(file, line)) { + const std::size_t sep = line.find('\t'); + if (sep == std::string::npos || sep == 0) { + continue; + } + st.aliases[line.substr(0, sep)] = line.substr(sep + 1); + } +} + +void store_aliases_locked(State& st) { + std::error_code error; + ensure_save_root(error); + std::ofstream file(save_root() / "_aliases", std::ios::binary | std::ios::trunc); + for (const auto& [alias, dir_name] : st.aliases) { + file << alias << '\t' << dir_name << '\n'; + } +} + +std::string save_alias_locked(State& st, const std::string& dir_name) { + if (dir_name.size() <= SD_MOUNT_POINT_CAPACITY) { + return dir_name; + } + load_aliases_locked(st); + const std::string base = dir_name.substr(0, 6); + std::uint32_t hash = 2166136261u; + for (const char c : dir_name) { + hash = (hash ^ static_cast(c)) * 16777619u; + } + for (std::uint32_t attempt = 0; attempt < 4096u; attempt++) { + char tail[8]; + std::snprintf(tail, sizeof(tail), "~%03X", static_cast((hash + attempt) & 0xFFFu)); + const std::string candidate = base + tail; + const auto found = st.aliases.find(candidate); + if (found == st.aliases.end()) { + st.aliases[candidate] = dir_name; + store_aliases_locked(st); + return candidate; + } + if (found->second == dir_name) { + return candidate; + } + } + return base; +} + +std::string real_dir_name_locked(State& st, const std::string& alias) { + load_aliases_locked(st); + const auto found = st.aliases.find(alias); + return found != st.aliases.end() ? found->second : alias; +} + +std::string mount_point_for(const std::string& alias) { + return std::string(SD_ROOT_GUEST) + "/" + alias; +} + +std::filesystem::path save_directory(const std::string& alias) { + return save_root() / alias; +} + +std::filesystem::path save_metadata_directory(const std::filesystem::path& directory) { + return directory / SD_SUBDIR; +} + +std::filesystem::path save_param_path(const std::filesystem::path& directory) { + return save_metadata_directory(directory) / "param.bin"; +} + +std::filesystem::path save_blocks_path(const std::filesystem::path& directory) { + return save_metadata_directory(directory) / "blocks.bin"; +} + +bool write_blob(const std::filesystem::path& path, const void* data, std::size_t size) { + std::error_code error; + std::filesystem::create_directories(path.parent_path(), error); + const std::filesystem::path temporary = path.native() + L".tmp"; + { + std::ofstream file(temporary, std::ios::binary | std::ios::trunc); + if (!file) { + return false; + } + if (size != 0) { + file.write(static_cast(data), static_cast(size)); + } + file.flush(); + if (!file) { + return false; + } + } + error.clear(); + std::filesystem::rename(temporary, path, error); + if (error) { + std::error_code ignored; + std::filesystem::remove(path, ignored); + error.clear(); + std::filesystem::rename(temporary, path, error); + } + std::error_code leftovers; + std::filesystem::remove(temporary, leftovers); + return !error; +} + +bool read_blob(const std::filesystem::path& path, void* data, std::size_t size) { + std::ifstream file(path, std::ios::binary); + if (!file) { + return false; + } + file.read(static_cast(data), static_cast(size)); + return static_cast(file.gcount()) == size; +} + +int write_u64(const std::filesystem::path& path, std::uint64_t value) { + return write_blob(path, &value, sizeof(value)) ? SD_OK : SD_ERROR_INTERNAL; +} + +int read_u64(const std::filesystem::path& path, std::uint64_t* value) { + std::error_code error; + if (!std::filesystem::is_regular_file(path, error) || error) { + return SD_ERROR_BROKEN; + } + if (std::filesystem::file_size(path, error) != sizeof(std::uint64_t) || error) { + return SD_ERROR_BROKEN; + } + return read_blob(path, value, sizeof(*value)) ? SD_OK : SD_ERROR_BROKEN; +} + +int load_save_param(const std::filesystem::path& directory, SaveDataParam* param) { + std::memset(param, 0, sizeof(*param)); + const int status = read_blob(save_param_path(directory), param, sizeof(*param)) ? SD_OK : SD_ERROR_NOT_FOUND; + if (status == SD_OK) { + std::int64_t newest = 0; + std::error_code error; + for (std::filesystem::recursive_directory_iterator it(directory, + std::filesystem::directory_options::skip_permission_denied, error), end; + it != end; it.increment(error)) { + if (error) { + error.clear(); + continue; + } + std::error_code file_error; + if (it->is_regular_file(file_error) && !file_error) { + const auto written = std::filesystem::last_write_time(it->path(), file_error); + if (!file_error) { + const auto seconds = std::chrono::duration_cast( + std::filesystem::file_time_type::clock::now() - written).count(); + const auto now = static_cast(std::time(nullptr)); + newest = std::max(newest, now - static_cast(seconds)); + } + } + } + param->mtime = newest; + } + return status; +} + +bool save_is_mounted_locked(const State& st, const std::filesystem::path& directory) { + for (const auto& slot : st.slots) { + if (slot.used && slot.directory == directory) { + return true; + } + } + return false; +} + +int slot_of_mount_point_locked(const State& st, const char* mount_point) { + for (std::size_t i = 0; i < st.slots.size(); i++) { + if (st.slots[i].used && st.slots[i].mount_point == mount_point) { + return static_cast(i); + } + } + return -1; +} + +int free_slot_locked(const State& st) { + for (std::size_t i = 0; i < st.slots.size(); i++) { + if (!st.slots[i].used) { + return static_cast(i); + } + } + return -1; +} + +void queue_event_locked(State& st, std::uint32_t type, std::int32_t user_id, + const SceSaveDataTitleId* title_id, const SceSaveDataDirName* dir_name, + int error_code, KernelEventFlag flag) { + PendingEvent pending; + pending.record = {}; + pending.record.type = type; + pending.record.error_code = error_code; + pending.record.user_id = user_id; + if (title_id != nullptr) { + std::memcpy(&pending.record.title_id, title_id, sizeof(pending.record.title_id)); + } + if (dir_name != nullptr) { + std::memcpy(&pending.record.dir_name, dir_name, sizeof(pending.record.dir_name)); + } + pending.flag = flag; + st.events.push_back(pending); + while (st.events.size() > 64) { + if (st.events.front().flag != nullptr) { + sceKernelDeleteEventFlag(st.events.front().flag); + } + st.events.pop_front(); + } +} + +KernelEventFlag create_signalled_flag(const char* name) { + KernelEventFlag flag = nullptr; + if (sceKernelCreateEventFlag(&flag, name, 0x20u, 0, nullptr) != 0) { + return nullptr; + } + sceKernelSetEventFlag(flag, 1u); + return flag; +} + +int initialize_internal(const void* init) { + State& st = state(); + std::lock_guard lock(st.mutex); + st.instances++; + return SD_OK; +} + +int terminate_internal() { + State& st = state(); + std::lock_guard lock(st.mutex); + if (st.instances == 0) { + return SD_ERROR_NOT_INITIALIZED; + } + if (std::any_of(st.slots.begin(), st.slots.end(), + [](const MountSlot& slot) { return slot.used; }) || !st.memory.empty()) { + return SD_ERROR_BUSY; + } + st.instances--; + if (st.instances == 0) { + for (auto& pending : st.events) { + if (pending.flag != nullptr) { + sceKernelDeleteEventFlag(pending.flag); + } + } + st.events.clear(); + st.memory.clear(); + st.live_transaction_resources.clear(); + } + return SD_OK; +} + +int mount_internal(const SceSaveDataDirName* dir_name, std::uint32_t mount_mode, + std::uint64_t blocks, std::int32_t user_id, SaveDataMountResult* mount_result, + const char* api) { + if (mount_result == nullptr) { + return SD_ERROR_PARAMETER; + } + std::memset(mount_result, 0, sizeof(*mount_result)); + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (dir_name == nullptr || !bounded_string_ok(dir_name->data, sizeof(dir_name->data)) || + !valid_dir_name(dir_name->data)) { + return SD_ERROR_PARAMETER; + } + if (user_id < 0) { + return SD_ERROR_INVALID_LOGIN_USER; + } + const bool create = (mount_mode & SD_MOUNT_CREATE) != 0; + const bool create_or_open = (mount_mode & SD_MOUNT_CREATE2) != 0; + const bool open = !create && !create_or_open && (mount_mode & (SD_MOUNT_RDONLY | SD_MOUNT_RDWR)) != 0; + if (!create && !create_or_open && !open) { + return SD_ERROR_PARAMETER; + } + + State& st = state(); + std::lock_guard lock(st.mutex); + const std::string requested = dir_name->data; + const std::string alias = save_alias_locked(st, requested); + const std::string mount_point = mount_point_for(alias); + if (mount_point.size() + 1 > sizeof(mount_result->mount_point)) { + return SD_ERROR_PARAMETER; + } + const std::filesystem::path directory = save_directory(alias); + for (const auto& slot : st.slots) { + if (slot.used && slot.directory == directory) { + return SD_ERROR_BUSY; + } + } + const int free_index = free_slot_locked(st); + if (free_index == -1) { + return SD_ERROR_MOUNT_FULL; + } + + std::error_code error; + const bool exists = std::filesystem::is_directory(directory, error) && !error; + if (open && !exists) { + return SD_ERROR_NOT_FOUND; + } + if (create && exists) { + return SD_ERROR_EXISTS; + } + bool created = false; + if ((create || create_or_open) && !exists) { + if (blocks < SD_BLOCKS_MIN || blocks > SD_BLOCKS_MAX) { + return SD_ERROR_PARAMETER; + } + std::error_code root_error; + if (!ensure_save_root(root_error)) { + return SD_ERROR_INTERNAL; + } + if (!std::filesystem::create_directories(directory, error) || error) { + return SD_ERROR_INTERNAL; + } + created = true; + if (write_u64(save_blocks_path(directory), blocks) != SD_OK) { + std::error_code cleanup; + std::filesystem::remove_all(directory, cleanup); + return SD_ERROR_INTERNAL; + } + SaveDataParam initial {}; + std::strncpy(initial.title, "Saved Data", sizeof(initial.title) - 1); + if (!write_blob(save_param_path(directory), &initial, sizeof(initial))) { + std::error_code cleanup; + std::filesystem::remove_all(directory, cleanup); + return SD_ERROR_INTERNAL; + } + } + if (exists && !created) { + std::uint64_t recorded = 0; + if (read_u64(save_blocks_path(directory), &recorded) != SD_OK && blocks != 0) { + if (blocks < SD_BLOCKS_MIN || blocks > SD_BLOCKS_MAX) { + return SD_ERROR_PARAMETER; + } + write_u64(save_blocks_path(directory), blocks); + } + } + + MountSlot& slot = st.slots[static_cast(free_index)]; + slot.used = true; + slot.mount_point = mount_point; + slot.dir_name = requested; + slot.directory = directory; + std::memcpy(mount_result->mount_point.data, mount_point.c_str(), mount_point.size() + 1); + mount_result->required_blocks = 0; + mount_result->mount_status = created ? 1u : 0u; + return SD_OK; +} + +int umount_internal(std::uint32_t mode, const SaveDataMountPoint* mount_point, const char* api) { + State& st = state(); + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (mount_point == nullptr || !bounded_string_ok(mount_point->data, sizeof(mount_point->data))) { + return SD_ERROR_PARAMETER; + } + std::lock_guard lock(st.mutex); + const int index = slot_of_mount_point_locked(st, mount_point->data); + if (index == -1) { + return SD_ERROR_NOT_MOUNTED; + } + st.slots[static_cast(index)] = MountSlot {}; + if ((mode & SD_UMOUNT_BACKUP_ASYNC) != 0) { + queue_event_locked(st, SD_EVENT_UMOUNT_BACKUP_END, 0, nullptr, nullptr, SD_OK, nullptr); + } + return SD_OK; +} + +int delete_internal(std::int32_t user_id, const SceSaveDataDirName* dir_name, + const SceSaveDataTitleId* title_id) { + State& st = state(); + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (dir_name == nullptr || !bounded_string_ok(dir_name->data, sizeof(dir_name->data)) || + !valid_dir_name(dir_name->data) || + (title_id != nullptr && !bounded_string_ok(title_id->data, sizeof(title_id->data)))) { + return SD_ERROR_PARAMETER; + } + if (user_id < 0) { + return SD_ERROR_INVALID_LOGIN_USER; + } + std::lock_guard lock(st.mutex); + const std::string alias = save_alias_locked(st, dir_name->data); + const std::filesystem::path directory = save_directory(alias); + if (save_is_mounted_locked(st, directory)) { + return SD_ERROR_BUSY; + } + std::error_code error; + if (!std::filesystem::exists(directory, error) || error) { + return SD_ERROR_NOT_FOUND; + } + std::filesystem::remove_all(directory, error); + if (error) { + return SD_ERROR_INTERNAL; + } + st.aliases.erase(alias); + if (alias != dir_name->data) { + store_aliases_locked(st); + } + return SD_OK; +} + +bool dir_name_matches(const char* str, const char* pattern) { + if (pattern == nullptr || pattern[0] == '\0') { + return true; + } + while (*str != '\0' && *pattern != '\0') { + if (*pattern == '%') { + for (const char* s = str;; s++) { + if (dir_name_matches(s, pattern + 1)) { + return true; + } + if (*s == '\0') { + break; + } + } + return false; + } + if (*pattern == '_') { + str++; + pattern++; + continue; + } + if (std::tolower(static_cast(*str)) != + std::tolower(static_cast(*pattern))) { + return false; + } + str++; + pattern++; + } + return *str == '\0' && *pattern == '\0'; +} + +int dir_name_search_internal(const SaveDataDirNameSearchCond* cond, + SaveDataDirNameSearchResult* result) { + State& st = state(); + if (result != nullptr) { + std::memset(result, 0, sizeof(*result)); + } + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (cond == nullptr || result == nullptr || + (result->dir_names_num != 0 && result->dir_names == nullptr) || + (cond->dir_name != nullptr && !bounded_string_ok(cond->dir_name->data, sizeof(cond->dir_name->data))) || + (cond->title_id != nullptr && !bounded_string_ok(cond->title_id->data, sizeof(cond->title_id->data)))) { + return SD_ERROR_PARAMETER; + } + if (cond->user_id < 0) { + return SD_ERROR_INVALID_LOGIN_USER; + } + std::lock_guard lock(st.mutex); + load_aliases_locked(st); + std::vector found; + std::error_code error; + if (std::filesystem::is_directory(save_root(), error) && !error) { + for (const auto& entry : std::filesystem::directory_iterator(save_root(), error)) { + if (error) { + break; + } + std::error_code kind_error; + if (!entry.is_directory(kind_error) || kind_error) { + continue; + } + const std::string alias = entry.path().filename().string(); + if (alias.empty() || alias[0] == '.' || alias[0] == '_' || + !valid_dir_name(alias.c_str())) { + continue; + } + std::error_code meta_error; + if (!std::filesystem::is_regular_file(save_param_path(entry.path()), meta_error) || meta_error) { + continue; + } + const std::string dir_name = real_dir_name_locked(st, alias); + if (cond->dir_name != nullptr && !dir_name_matches(dir_name.c_str(), cond->dir_name->data)) { + continue; + } + found.push_back(dir_name); + } + } + std::sort(found.begin(), found.end()); + if (cond->order == 1) { + std::reverse(found.begin(), found.end()); + } + result->hit_num = static_cast(found.size()); + const std::uint32_t set_count = std::min(result->dir_names_num, found.size()); + for (std::uint32_t i = 0; i < set_count; i++) { + std::snprintf(result->dir_names[i].data, sizeof(result->dir_names[i].data), "%s", + found[i].c_str()); + if (result->params != nullptr) { + load_save_param(save_directory(save_alias_locked(st, found[i])), &result->params[i]); + } + if (result->infos != nullptr) { + std::memset(&result->infos[i], 0, sizeof(result->infos[i])); + std::uint64_t blocks = 0; + if (read_u64(save_blocks_path(save_directory(save_alias_locked(st, found[i]))), &blocks) == SD_OK) { + std::memcpy(&result->infos[i], &blocks, sizeof(blocks)); + } + } + } + result->set_num = set_count; + return SD_OK; +} + +int get_param_internal(const SaveDataMountPoint* mount_point, std::uint32_t param_type, + void* param_buf, std::size_t param_buf_size, std::size_t* got_size) { + State& st = state(); + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (mount_point == nullptr || param_buf == nullptr || param_type > SD_PARAM_MTIME || + !bounded_string_ok(mount_point->data, sizeof(mount_point->data))) { + return SD_ERROR_PARAMETER; + } + std::lock_guard lock(st.mutex); + const int index = slot_of_mount_point_locked(st, mount_point->data); + if (index == -1) { + return SD_ERROR_NOT_MOUNTED; + } + SaveDataParam param {}; + const int status = load_save_param(st.slots[static_cast(index)].directory, ¶m); + if (status != SD_OK) { + return status; + } + std::size_t written = 0; + if (param_type == SD_PARAM_WHOLE) { + if (param_buf_size < sizeof(param)) { + return SD_ERROR_PARAMETER; + } + std::memcpy(param_buf, ¶m, sizeof(param)); + written = sizeof(param); + } else if (param_type == SD_PARAM_USER_PARAM || param_type == SD_PARAM_MTIME) { + const std::size_t size = param_type == SD_PARAM_USER_PARAM ? sizeof(param.user_param) + : sizeof(param.mtime); + if (param_buf_size < size) { + return SD_ERROR_PARAMETER; + } + std::memcpy(param_buf, + param_type == SD_PARAM_USER_PARAM + ? static_cast(¶m.user_param) + : static_cast(¶m.mtime), + size); + written = size; + } else { + char* field = param_type == SD_PARAM_TITLE ? param.title + : param_type == SD_PARAM_SUB_TITLE ? param.sub_title : param.detail; + const std::size_t capacity = param_type == SD_PARAM_DETAIL ? sizeof(param.detail) + : sizeof(param.title); + if (param_buf_size == 0) { + return SD_ERROR_PARAMETER; + } + const std::size_t length = std::min(std::strlen(field), param_buf_size - 1); + std::memcpy(param_buf, field, length); + static_cast(param_buf)[length] = '\0'; + written = length + 1; + } + if (got_size != nullptr) { + *got_size = written; + } + return SD_OK; +} + +int set_param_internal(const SaveDataMountPoint* mount_point, std::uint32_t param_type, + const void* param_buf, std::size_t param_buf_size) { + State& st = state(); + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (mount_point == nullptr || param_buf == nullptr || param_type > SD_PARAM_MTIME || + !bounded_string_ok(mount_point->data, sizeof(mount_point->data))) { + return SD_ERROR_PARAMETER; + } + std::lock_guard lock(st.mutex); + const int index = slot_of_mount_point_locked(st, mount_point->data); + if (index == -1) { + return SD_ERROR_NOT_MOUNTED; + } + const std::filesystem::path directory = st.slots[static_cast(index)].directory; + if (param_type == SD_PARAM_WHOLE) { + if (param_buf_size < sizeof(SaveDataParam)) { + return SD_ERROR_PARAMETER; + } + SaveDataParam param = *static_cast(param_buf); + param.mtime = static_cast(std::time(nullptr)); + return write_blob(save_param_path(directory), ¶m, sizeof(param)) ? SD_OK : SD_ERROR_INTERNAL; + } + SaveDataParam param {}; + const int status = load_save_param(directory, ¶m); + if (status != SD_OK && status != SD_ERROR_NOT_FOUND) { + return status; + } + if (param_type == SD_PARAM_USER_PARAM || param_type == SD_PARAM_MTIME) { + const std::size_t size = param_type == SD_PARAM_USER_PARAM ? sizeof(param.user_param) + : sizeof(param.mtime); + if (param_buf_size < size) { + return SD_ERROR_PARAMETER; + } + std::memcpy(param_type == SD_PARAM_USER_PARAM ? static_cast(¶m.user_param) + : static_cast(¶m.mtime), + param_buf, size); + } else { + char* field = param_type == SD_PARAM_TITLE ? param.title + : param_type == SD_PARAM_SUB_TITLE ? param.sub_title : param.detail; + const std::size_t capacity = param_type == SD_PARAM_DETAIL ? sizeof(param.detail) + : sizeof(param.title); + if (param_buf_size == 0) { + return SD_ERROR_PARAMETER; + } + const std::size_t length = std::min(param_buf_size, capacity - 1); + const char* text = static_cast(param_buf); + const char* end = static_cast(std::memchr(text, '\0', length)); + std::memset(field, 0, capacity); + std::memcpy(field, text, end != nullptr ? static_cast(end - text) : length); + } + return write_blob(save_param_path(directory), ¶m, sizeof(param)) ? SD_OK : SD_ERROR_INTERNAL; +} + +int get_mount_info_internal(const SaveDataMountPoint* mount_point, SaveDataMountInfo* info) { + State& st = state(); + if (info != nullptr) { + std::memset(info, 0, sizeof(*info)); + } + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (mount_point == nullptr || info == nullptr || + !bounded_string_ok(mount_point->data, sizeof(mount_point->data))) { + return SD_ERROR_PARAMETER; + } + std::lock_guard lock(st.mutex); + const int index = slot_of_mount_point_locked(st, mount_point->data); + if (index == -1) { + return SD_ERROR_NOT_MOUNTED; + } + std::uint64_t blocks = 0; + const int status = read_u64(save_blocks_path(st.slots[static_cast(index)].directory), + &blocks); + if (status != SD_OK) { + return status; + } + info->blocks = blocks; + info->free_blocks = blocks; + return SD_OK; +} + +int is_mounted_internal(const SaveDataMountPoint* mount_point, std::uint32_t* is_mounted) { + if (mount_point == nullptr || is_mounted == nullptr || + !bounded_string_ok(mount_point->data, sizeof(mount_point->data))) { + return SD_ERROR_PARAMETER; + } + State& st = state(); + std::lock_guard lock(st.mutex); + *is_mounted = slot_of_mount_point_locked(st, mount_point->data) != -1 ? 1u : 0u; + return SD_OK; +} + +std::int64_t count_mounted_internal() { + State& st = state(); + std::lock_guard lock(st.mutex); + std::int64_t count = 0; + for (const auto& slot : st.slots) { + if (slot.used) { + count++; + } + } + return count; +} + +std::string memory_key(std::int32_t user_id, std::uint32_t slot_id) { + char key[32]; + std::snprintf(key, sizeof(key), "%d:%u", static_cast(user_id), static_cast(slot_id)); + return key; +} + +std::filesystem::path memory_directory(std::int32_t user_id, std::uint32_t slot_id) { + char name[48]; + std::snprintf(name, sizeof(name), "%s%s", SD_MEMORY_PREFIX, + slot_id == 0 ? "" : std::to_string(slot_id).c_str()); + return save_root() / name / std::to_string(static_cast(user_id)); +} + +bool valid_memory_range(const MemoryData& data, std::size_t size) { + return data.buf != nullptr && data.offset >= 0 && + static_cast(data.offset) <= size && + data.buf_size <= size - static_cast(data.offset); +} + +int setup_memory_internal(const SaveDataMemorySetup2* setup, SaveDataMemorySetupResult* result, + KernelEventFlag flag) { + State& st = state(); + if (setup == nullptr || setup->slot_id >= SD_MEMORY_SLOTS || setup->memory_size == 0 || + setup->memory_size > SD_MEMORY_MAX_SIZE || (setup->option & ~(SD_MEMORY_SET_PARAM | SD_MEMORY_DOUBLE_BUFFER)) != 0 || + (setup->init_param != nullptr && setup->option != 0 && (setup->option & SD_MEMORY_SET_PARAM) == 0)) { + return SD_ERROR_PARAMETER; + } + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (setup->user_id < 0) { + return SD_ERROR_INVALID_LOGIN_USER; + } + std::lock_guard lock(st.mutex); + const std::string key = memory_key(setup->user_id, setup->slot_id); + if (st.memory.count(key) != 0) { + return SD_ERROR_BUSY; + } + if (st.memory.size() >= SD_MEMORY_MAX_MOUNTED) { + return SD_ERROR_LIMITATION_OVER; + } + if (save_is_mounted_locked(st, memory_directory(setup->user_id, setup->slot_id))) { + return SD_ERROR_BUSY; + } + MemorySlot slot; + slot.option = setup->option; + slot.data.resize(setup->memory_size, 0); + std::strncpy(slot.param.title, "Saved Data", sizeof(slot.param.title) - 1); + std::size_t existed = 0; + std::error_code error; + const std::filesystem::path blob = memory_directory(setup->user_id, setup->slot_id) / "memory.dat"; + if (std::filesystem::is_regular_file(blob, error) && !error) { + const std::size_t stored = static_cast(std::filesystem::file_size(blob, error)); + if (error || stored > SD_MEMORY_MAX_SIZE) { + return SD_ERROR_BROKEN; + } + existed = stored; + std::vector loaded(stored, 0); + std::ifstream file(blob, std::ios::binary); + file.read(loaded.data(), static_cast(stored)); + if (!file) { + return SD_ERROR_BROKEN; + } + std::copy_n(loaded.begin(), std::min(stored, slot.data.size()), slot.data.begin()); + load_save_param(memory_directory(setup->user_id, setup->slot_id), &slot.param); + } + if (existed == 0 && setup->init_param != nullptr && (setup->option & SD_MEMORY_SET_PARAM) != 0) { + slot.param = *setup->init_param; + slot.param.mtime = 0; + } + st.memory.emplace(key, std::move(slot)); + if (result != nullptr) { + std::memset(result, 0, sizeof(*result)); + result->existed_memory_size = existed; + } + if (flag != nullptr) { + queue_event_locked(st, SD_EVENT_MEMORY_SYNC_END, setup->user_id, nullptr, nullptr, SD_OK, flag); + } + return SD_OK; +} + +int get_memory_internal(SaveDataMemoryGet2* get_param) { + State& st = state(); + if (get_param == nullptr || get_param->slot_id >= SD_MEMORY_SLOTS) { + return SD_ERROR_PARAMETER; + } + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (get_param->user_id < 0) { + return SD_ERROR_INVALID_LOGIN_USER; + } + std::lock_guard lock(st.mutex); + const auto it = st.memory.find(memory_key(get_param->user_id, get_param->slot_id)); + if (it == st.memory.end()) { + return SD_ERROR_MEMORY_NOT_READY; + } + const MemorySlot& slot = it->second; + if (get_param->data != nullptr && + !valid_memory_range(*reinterpret_cast(get_param->data), slot.data.size())) { + return SD_ERROR_PARAMETER; + } + if (get_param->param != nullptr && (slot.option & SD_MEMORY_SET_PARAM) == 0) { + return SD_ERROR_PARAMETER; + } + if (get_param->data != nullptr) { + const MemoryData& data = *reinterpret_cast(get_param->data); + std::memcpy(data.buf, slot.data.data() + data.offset, data.buf_size); + } + if (get_param->param != nullptr) { + *get_param->param = slot.param; + } + if (get_param->icon != nullptr) { + get_param->icon->data_size = 0; + } + return SD_OK; +} + +int set_memory_internal(const SaveDataMemorySet2* set_param) { + State& st = state(); + if (set_param == nullptr || set_param->slot_id >= SD_MEMORY_SLOTS || + set_param->data_num > SD_MEMORY_MAX_DATA_NUM || + (set_param->data == nullptr && set_param->data_num != 0)) { + return SD_ERROR_PARAMETER; + } + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (set_param->user_id < 0) { + return SD_ERROR_INVALID_LOGIN_USER; + } + std::lock_guard lock(st.mutex); + const auto it = st.memory.find(memory_key(set_param->user_id, set_param->slot_id)); + if (it == st.memory.end()) { + return SD_ERROR_MEMORY_NOT_READY; + } + if (set_param->param != nullptr && (it->second.option & SD_MEMORY_SET_PARAM) == 0) { + return SD_ERROR_PARAMETER; + } + const std::uint32_t count = + set_param->data != nullptr && set_param->data_num == 0 ? 1u : set_param->data_num; + const MemoryData* descriptors = reinterpret_cast(set_param->data); + for (std::uint32_t i = 0; i < count; i++) { + if (!valid_memory_range(descriptors[i], it->second.data.size())) { + return SD_ERROR_PARAMETER; + } + } + MemorySlot updated = it->second; + for (std::uint32_t i = 0; i < count; i++) { + const MemoryData& data = descriptors[i]; + std::memcpy(updated.data.data() + data.offset, data.buf, data.buf_size); + } + if (set_param->param != nullptr) { + updated.param = *set_param->param; + } + updated.param.mtime = static_cast(std::time(nullptr)); + updated.dirty = true; + it->second = std::move(updated); + return SD_OK; +} + +int sync_memory_internal(const std::int32_t user_id, const std::uint32_t slot_id, + const std::uint32_t option, KernelEventFlag flag, bool report_event) { + State& st = state(); + if (slot_id >= SD_MEMORY_SLOTS || option > 1) { + return SD_ERROR_PARAMETER; + } + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (user_id < 0) { + return SD_ERROR_INVALID_LOGIN_USER; + } + std::lock_guard lock(st.mutex); + const auto it = st.memory.find(memory_key(user_id, slot_id)); + if (it == st.memory.end()) { + return SD_ERROR_MEMORY_NOT_READY; + } + const std::filesystem::path directory = memory_directory(user_id, slot_id); + int status = SD_OK; + if (!write_blob(directory / "memory.dat", it->second.data.data(), it->second.data.size())) { + status = SD_ERROR_INTERNAL; + } else if (!write_blob(save_param_path(directory), &it->second.param, sizeof(it->second.param))) { + status = SD_ERROR_INTERNAL; + } else { + it->second.dirty = false; + } + if (option == 1 || !report_event) { + return status; + } + char title_text[sizeof(SceSaveDataTitleId)] = {}; + char dir_text[sizeof(SceSaveDataDirName)] = {}; + std::snprintf(dir_text, sizeof(dir_text), "%s%s", SD_MEMORY_PREFIX, + slot_id == 0 ? "" : std::to_string(slot_id).c_str()); + SceSaveDataTitleId title {}; + SceSaveDataDirName directory_name {}; + std::memcpy(title.data, title_text, sizeof(title.data)); + std::memcpy(directory_name.data, dir_text, sizeof(directory_name.data)); + queue_event_locked(st, SD_EVENT_MEMORY_SYNC_END, user_id, &title, &directory_name, status, flag); + return SD_OK; +} + +int commit_internal(const SaveDataCommitParam* param) { + State& st = state(); + if (param == nullptr) { + return SD_ERROR_PARAMETER; + } + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + std::lock_guard lock(st.mutex); + for (auto& [key, slot] : st.memory) { + if (!slot.dirty) { + continue; + } + const std::size_t sep = key.find(':'); + const std::int32_t user_id = static_cast(std::strtol(key.c_str(), nullptr, 10)); + const std::uint32_t slot_id = sep == std::string::npos + ? 0u + : static_cast(std::strtoul(key.c_str() + sep + 1, nullptr, 10)); + const std::filesystem::path directory = memory_directory(user_id, slot_id); + if (!write_blob(directory / "memory.dat", slot.data.data(), slot.data.size()) || + !write_blob(save_param_path(directory), &slot.param, sizeof(slot.param))) { + return SD_ERROR_INTERNAL; + } + slot.dirty = false; + } + if ((param->commit_mode & SD_COMMIT_BACKUP_ASYNC) != 0) { + queue_event_locked(st, SD_EVENT_COMMIT_BACKUP_END, 0, nullptr, nullptr, SD_OK, nullptr); + } + return SD_OK; +} + +int prepare_internal(const SaveDataMountPoint* mount_point, const SaveDataPrepareParam* param) { + if (mount_point == nullptr || param == nullptr || + !bounded_string_ok(mount_point->data, sizeof(mount_point->data))) { + return SD_ERROR_PARAMETER; + } + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + return SD_OK; +} + +int backup_internal(const SaveDataBackup* backup, KernelEventFlag flag) { + State& st = state(); + if (backup == nullptr || backup->dir_name == nullptr || + !bounded_string_ok(backup->dir_name->data, sizeof(backup->dir_name->data))) { + return SD_ERROR_PARAMETER; + } + if (backup->title_id != nullptr && !bounded_string_ok(backup->title_id->data, sizeof(backup->title_id->data))) { + return SD_ERROR_PARAMETER; + } + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (backup->user_id < 0) { + return SD_ERROR_INVALID_LOGIN_USER; + } + std::lock_guard lock(st.mutex); + queue_event_locked(st, SD_EVENT_BACKUP_END, backup->user_id, backup->title_id, backup->dir_name, + SD_OK, flag); + return SD_OK; +} + +int transferring_mount_internal(const SaveDataTransferringMount* mount, + SaveDataMountResult* mount_result) { + if (mount_result != nullptr) { + std::memset(mount_result, 0, sizeof(*mount_result)); + } + State& st = state(); + if (mount == nullptr || mount_result == nullptr || mount->title_id == nullptr || + mount->dir_name == nullptr || + !bounded_string_ok(mount->title_id->data, sizeof(mount->title_id->data)) || + !bounded_string_ok(mount->dir_name->data, sizeof(mount->dir_name->data))) { + return SD_ERROR_PARAMETER; + } + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (mount->user_id < 0) { + return SD_ERROR_INVALID_LOGIN_USER; + } + if (!valid_dir_name(mount->title_id->data) || !valid_dir_name(mount->dir_name->data)) { + return SD_ERROR_PARAMETER; + } + std::lock_guard lock(st.mutex); + const std::filesystem::path directory = save_root() / "transfer" / mount->title_id->data / + mount->dir_name->data; + std::error_code error; + if (!std::filesystem::is_directory(directory, error) || error) { + return SD_ERROR_NOT_FOUND; + } + const int index = free_slot_locked(st); + if (index == -1) { + return SD_ERROR_MOUNT_FULL; + } + for (const auto& slot : st.slots) { + if (slot.used && slot.directory == directory) { + return SD_ERROR_BUSY; + } + } + const std::string mount_point = mount_point_for(save_alias_locked( + st, std::string(mount->title_id->data) + "-" + mount->dir_name->data)); + if (mount_point.size() + 1 > sizeof(mount_result->mount_point)) { + return SD_ERROR_PARAMETER; + } + MountSlot& slot = st.slots[static_cast(index)]; + slot.used = true; + slot.mount_point = mount_point; + slot.dir_name = mount->dir_name->data; + slot.directory = directory; + std::memcpy(mount_result->mount_point.data, mount_point.c_str(), mount_point.size() + 1); + mount_result->required_blocks = 0; + mount_result->mount_status = 0; + return SD_OK; +} + +int get_event_result_internal(const void* event_param, SaveDataEvent* event) { + (void)event_param; + State& st = state(); + if (event == nullptr) { + return SD_ERROR_PARAMETER; + } + std::lock_guard lock(st.mutex); + if (st.events.empty()) { + return SD_ERROR_NOT_FOUND; + } + const PendingEvent pending = st.events.front(); + st.events.pop_front(); + if (pending.flag != nullptr) { + sceKernelDeleteEventFlag(pending.flag); + } + *event = pending.record; + return event->error_code; +} + +int cancel_internal(KernelEventFlag event) { + State& st = state(); + if (event == nullptr) { + return SD_ERROR_PARAMETER; + } + std::lock_guard lock(st.mutex); + for (auto it = st.events.begin(); it != st.events.end(); ++it) { + if (it->flag == event) { + sceKernelCancelEventFlag(event, 0, nullptr); + const KernelEventFlag flag = it->flag; + st.events.erase(it); + sceKernelDeleteEventFlag(flag); + return SD_OK; + } + } + return SD_ERROR_NOT_FOUND; +} + +int abort_internal() { + State& st = state(); + std::lock_guard lock(st.mutex); + for (auto& pending : st.events) { + if (pending.flag != nullptr) { + sceKernelCancelEventFlag(pending.flag, 0, nullptr); + sceKernelDeleteEventFlag(pending.flag); + pending.flag = nullptr; + } + } + st.events.clear(); + return SD_OK; +} + +int create_transaction_resource_internal(std::uint32_t size) { + State& st = state(); + std::lock_guard lock(st.mutex); + if (st.next_transaction_resource <= 0) { + return SD_ERROR_OUT_OF_MEMORY; + } + const std::int32_t id = st.next_transaction_resource++; + st.live_transaction_resources.insert(id); + return id; +} + +int delete_transaction_resource_internal(std::int32_t resource) { + State& st = state(); + std::lock_guard lock(st.mutex); + return st.live_transaction_resources.erase(resource) != 0 ? SD_OK : SD_ERROR_PARAMETER; +} + +std::filesystem::path app_status_path(const std::string& dir_name) { + return save_root() / "_app_status" / dir_name; +} + +int get_local_storage_app_status_internal(const SceSaveDataDirName* dir_name, + const SceSaveDataTitleId* title_id, + std::uint32_t* status) { + if (status == nullptr || dir_name == nullptr || + !bounded_string_ok(dir_name->data, sizeof(dir_name->data)) || + (title_id != nullptr && !bounded_string_ok(title_id->data, sizeof(title_id->data)))) { + return SD_ERROR_PARAMETER; + } + State& st = state(); + std::lock_guard lock(st.mutex); + *status = SD_APP_STATUS_NONE; + std::error_code error; + const std::filesystem::path path = app_status_path(dir_name->data); + if (std::filesystem::is_regular_file(path, error) && !error) { + std::uint32_t stored = 0; + std::ifstream file(path, std::ios::binary); + file.read(reinterpret_cast(&stored), sizeof(stored)); + if (!file) { + return SD_ERROR_BROKEN; + } + *status = stored; + return SD_OK; + } + const std::string alias = save_alias_locked(st, dir_name->data); + if (std::filesystem::is_directory(save_directory(alias), error) && !error) { + *status = SD_APP_STATUS_LOCAL; + return SD_OK; + } + *status = SD_APP_STATUS_NONE; + return SD_OK; +} + +int set_local_storage_app_status_internal(const SceSaveDataDirName* dir_name, + const SceSaveDataTitleId* title_id, + std::uint32_t status) { + if (dir_name == nullptr || !bounded_string_ok(dir_name->data, sizeof(dir_name->data)) || + (title_id != nullptr && !bounded_string_ok(title_id->data, sizeof(title_id->data))) || + (status & ~(SD_APP_STATUS_DELETED | SD_APP_STATUS_LOCAL | SD_APP_STATUS_ONLINE)) != 0) { + return SD_ERROR_PARAMETER; + } + std::error_code error; + std::filesystem::create_directories(app_status_path(dir_name->data).parent_path(), error); + if (error) { + return SD_ERROR_INTERNAL; + } + return write_blob(app_status_path(dir_name->data), &status, sizeof(status)) ? SD_OK + : SD_ERROR_INTERNAL; +} + +int delete_local_storage_app_status_internal(const SceSaveDataDirName* dir_name, + const SceSaveDataTitleId* title_id) { + if (dir_name == nullptr || !bounded_string_ok(dir_name->data, sizeof(dir_name->data)) || + (title_id != nullptr && !bounded_string_ok(title_id->data, sizeof(title_id->data)))) { + return SD_ERROR_PARAMETER; + } + std::error_code error; + std::filesystem::remove(app_status_path(dir_name->data), error); + return error ? SD_ERROR_INTERNAL : SD_OK; +} + +int check_internal() { + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + std::error_code error; + return ensure_save_root(error) ? SD_OK : SD_ERROR_INTERNAL; +} + +std::uint64_t get_all_size_internal(std::int32_t user_id) { + State& st = state(); + std::lock_guard lock(st.mutex); + std::uint64_t total = 0; + for (const auto& [key, slot] : st.memory) { + total += slot.data.size() * ((slot.option & SD_MEMORY_DOUBLE_BUFFER) != 0 ? 2u : 1u); + } + return total; +} + +} // namespace +} // namespace savedata + +#endif diff --git a/core/libs/prx/libSceSaveDataDialog/Export.cpp b/core/libs/prx/libSceSaveDataDialog/Export.cpp index 1b0bcb415..068c96f14 100644 --- a/core/libs/prx/libSceSaveDataDialog/Export.cpp +++ b/core/libs/prx/libSceSaveDataDialog/Export.cpp @@ -1,55 +1,67 @@ #include +#include #include +#include #include "SceTypes.hpp" +#include "prx/libSceSaveDataDialog.native/SaveDataDialog.hpp" #include "prx/libc/include/General.hpp" + +namespace { +// Throwing from these reaches std::terminate and aborts the game process. +int SoftFailNp(const char* func) { + std::fprintf(stderr, "[SOFT-NP] %s -> 0 (unimplemented SCE API - no-op)\n", func); + std::fflush(stderr); + return 0; +} +} // namespace + extern "C" { int APS5_VABI sceSaveDataDialogClose(const void* close_param) { (void)close_param; - NotImplemented_nid_no_patch(__func__); - return 0; + return SoftFailNp(__func__); } int APS5_VABI sceSaveDataDialogGetResult(void* result) { - (void)result; - NotImplemented_nid_no_patch(__func__); - return 0; + // Never return 0 with an untouched out-struct: the game would assert on the garbage later. + if (result == nullptr) { + return SAVE_DATA_DIALOG_ERROR_ARG_NULL; + } + auto* r = static_cast(result); + std::memset(r, 0, sizeof(*r)); + r->result = SAVE_DATA_DIALOG_RESULT_OK; + r->button_id = SAVE_DATA_DIALOG_BUTTON_ID_OK; + return SoftFailNp(__func__); } int APS5_VABI sceSaveDataDialogInitialize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SoftFailNp(__func__); } int APS5_VABI sceSaveDataDialogIsReadyToDisplay(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SoftFailNp(__func__); } int APS5_VABI sceSaveDataDialogOpen(const void* param) { (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; + return SoftFailNp(__func__); } int APS5_VABI sceSaveDataDialogProgressBarInc(int target, uint32_t delta) { (void)target; (void)delta; - NotImplemented_nid_no_patch(__func__); - return 0; + return SoftFailNp(__func__); } int APS5_VABI sceSaveDataDialogProgressBarSetValue(int target, uint32_t rate) { (void)target; (void)rate; - NotImplemented_nid_no_patch(__func__); - return 0; + return SoftFailNp(__func__); } int APS5_VABI sceSaveDataDialogTerminate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SoftFailNp(__func__); } } From 8cef0341722c1ec927c3c2893e9559d0e2a10c25 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Mon, 28 Sep 2026 19:54:01 +1000 Subject: [PATCH 13/46] fix(shader): safely destroy compiler thread-local storage on Windows --- core/libs/tests/HostThreadLocal.cpp | 12 ++++++++++++ core/shader/recompiler/CMakeLists.txt | 1 + core/shader/recompiler/CacheKey.hpp | 4 +++- .../src/SrtWalker/SrtDescriptorEvaluation.cpp | 12 ++++++++---- core/shader/recompiler/Recompiler.cpp | 3 ++- .../Translation/src/ShaderInputInfoBuilder.cpp | 12 +++++++----- 6 files changed, 33 insertions(+), 11 deletions(-) diff --git a/core/libs/tests/HostThreadLocal.cpp b/core/libs/tests/HostThreadLocal.cpp index 4fedf466a..0a3639bd2 100644 --- a/core/libs/tests/HostThreadLocal.cpp +++ b/core/libs/tests/HostThreadLocal.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include #ifdef _WIN32 #include @@ -21,4 +23,14 @@ int main() { if (DestroyedHostThreadLocals() != 128 + (i + 1) * 2) std::abort(); } #endif + for (unsigned i = 0; i < 16; ++i) { + const auto before = DestroyedHostThreadLocals(); + std::array, 4> workers; + for (auto& worker : workers) { + worker = std::async(std::launch::async, [] { TouchHostThreadLocal(); TouchHostThreadLocal(); }); + } + for (auto& worker : workers) worker.get(); + if (DestroyedHostThreadLocals() != before + 8) std::abort(); + } + } diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index e9f394071..33751b2a5 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -138,6 +138,7 @@ function(add_shader_recompiler target runtimeTarget) ) target_include_directories(${target} PUBLIC + ${PROJECT_SOURCE_DIR}/core/libs ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/ ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/RdnaDecoder/include ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/ControlFlow/include diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 1e6fc2025..24e4b66b4 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -2,6 +2,7 @@ #define CORE_SHADER_RECOMPILER_CACHEKEY_HPP #include "Recompiler.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include #include #include @@ -37,7 +38,8 @@ public: // key of a source memo whose owner fixes the code (a registered shader at an offset) and the // device itself, and which is bypassed while the probe is active. static std::uint64_t ContextHash(const RecompileRequest& request) { - thread_local std::vector key; + struct ContextKeyStorage {}; + auto& key = HostThreadLocal, ContextKeyStorage>(); key.clear(); append(key, request.shader.stage); append(key, request.context.waveSize); diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp index ee1e7c6d6..7ea4c4a78 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp @@ -1,5 +1,6 @@ #include "Optimization/SrtWalker/SrtDescriptorEvaluation.hpp" #include "Optimization/SrtWalker/SrtEvaluator.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include #include @@ -10,7 +11,10 @@ namespace ShaderRecompiler::Detail { namespace { -thread_local std::string g_failureReason; +std::string& failureReason() { + struct FailureReasonStorage {}; + return HostThreadLocal(); +} std::string DescribeValue(const IrValue* value, std::uint32_t depth) { if (value == nullptr) return "null"; @@ -27,7 +31,7 @@ std::string DescribeValue(const IrValue* value, std::uint32_t depth) { } bool Fail(std::string reason) { - g_failureReason = std::move(reason); + failureReason() = std::move(reason); return false; } @@ -41,7 +45,7 @@ const DescriptorSource* Source(const IrResourcePlan& program, std::uint32_t sour } bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span sources, const SrtRuntime& runtime, std::vector& results, std::vector& flat, bool evaluateFlat, std::span cleanFlatSlots, std::vector& activeSources) { - g_failureReason.clear(); + failureReason().clear(); static const bool debug = std::getenv("APS5_SRT_DEBUG") != nullptr; if (debug) { for (std::size_t slot = 0; slot < program.srtReads.size(); ++slot) std::fprintf(stderr, "[srt] slot %zu = %s" "\n", slot, DescribeValue(program.srtReads[slot].value, 6).c_str()); @@ -143,7 +147,7 @@ bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span getSource(const RecompileRequest& request) { static std::shared_mutex mutex; // Entries whose code hashes alike share a bucket; the code comparison picks the right one. static std::unordered_map, std::vector>, SourceKeyHash> sources; - thread_local std::vector key; + struct SourceKeyStorage {}; + auto& key = HostThreadLocal, SourceKeyStorage>(); RecompileCacheKey::Build(request, key); const auto find = [&]() -> std::shared_ptr { const auto found = sources.find(key); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 92fe17ef0..ecd870cdc 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -1,4 +1,5 @@ #include "Translation/ShaderInputInfoBuilder.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include "IntermediateRepresentation/IrMetadata.hpp" #include #include @@ -7,11 +8,6 @@ namespace ShaderRecompiler { namespace { - -thread_local ShaderPixelInputInfo pixelStorage; -thread_local ShaderComputeInputInfo computeStorage; -thread_local ShaderVertexInputInfo vertexStorage; - IrShaderStage _toIrShaderStage(ShaderStageKind stage) { switch (stage) { case ShaderStageKind::Vertex: return IrShaderStage::Vertex; @@ -74,6 +70,8 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues throw std::runtime_error("ShaderInputInfoBuilder: GuestContext.compute is not set"); } const auto& compute = *context.compute; + struct ComputeStorage {}; + auto& computeStorage = HostThreadLocal(); computeStorage = ShaderComputeInputInfo{}; computeStorage.threadsNum[0] = compute.numThreads[0]; computeStorage.threadsNum[1] = compute.numThreads[1]; @@ -97,6 +95,8 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues throw std::runtime_error("ShaderInputInfoBuilder: GuestContext.pixel is not set"); } const auto& pixel = *context.pixel; + struct PixelStorage {}; + auto& pixelStorage = HostThreadLocal(); pixelStorage = ShaderPixelInputInfo{}; for (std::uint32_t i = 0; i < 32; ++i) { pixelStorage.interpolatorSettings[i] = pixel.interpolatorSettings[i]; @@ -136,6 +136,8 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues } const auto& vertex = *context.vertex; if (vertex.resourcesNum > vertex.resources.size()) throw std::runtime_error("ShaderInputInfoBuilder: invalid vertex resource count"); + struct VertexStorage {}; + auto& vertexStorage = HostThreadLocal(); vertexStorage = ShaderVertexInputInfo{}; vertexStorage.logicalStage = _toIrShaderStage(stage); vertexStorage.fetchEmbedded = vertex.fetchEmbedded; From 73e031a4dd1e478a248d9a9c15b20ec93f97030a Mon Sep 17 00:00:00 2001 From: boykopovar Date: Mon, 28 Sep 2026 13:06:15 +0300 Subject: [PATCH 14/46] perf(agc): downsample frame captures on GPU and queue disk writes --- .../Execution/include/PresentationScaler.hpp | 1 + .../Execution/src/PresentationScaler.cpp | 9 +- .../Execution/src/VulkanDevice.cpp | 145 ++++++++++++++---- 3 files changed, 124 insertions(+), 31 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp index c3fb1e15c..d56076311 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp @@ -21,6 +21,7 @@ public: // Fills the source image from another image of the same extent (any blittable format, layout // unchanged), leaving the source image ready to blit or read back. void RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter); + void RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter, std::uint32_t width, std::uint32_t height); // Copies the source image's texels (tightly packed rows of its format) into a host-readable buffer // and makes them visible to the host once the submission completes. void RecordReadback(VkCommandBuffer commands, VkBuffer destination); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp index aa7cd9bc9..ecea37cbe 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp @@ -79,7 +79,12 @@ void PresentationScaler::RecordUpload(VkCommandBuffer commands, VkBuffer uploadB } void PresentationScaler::RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter) { + RecordBlitInto(commands, image, layout, filter, sourceWidth, sourceHeight); +} + +void PresentationScaler::RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter, std::uint32_t width, std::uint32_t height) { Graphics::Require(sourceImage != VK_NULL_HANDLE && image != VK_NULL_HANDLE, "presentation image is unavailable"); + Graphics::Require(width != 0 && height != 0, "presentation blit extent must be non-zero"); const auto pipelineBarrier = context.Function("vkCmdPipelineBarrier"); VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; @@ -92,9 +97,9 @@ void PresentationScaler::RecordBlitInto(VkCommandBuffer commands, VkImage image, pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); VkImageBlit blit{}; blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; - blit.srcOffsets[1] = {static_cast(sourceWidth), static_cast(sourceHeight), 1}; + blit.srcOffsets[1] = {static_cast(width), static_cast(height), 1}; blit.dstSubresource = blit.srcSubresource; - blit.dstOffsets[1] = blit.srcOffsets[1]; + blit.dstOffsets[1] = {static_cast(sourceWidth), static_cast(sourceHeight), 1}; context.Function("vkCmdBlitImage")(commands, image, layout, sourceImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, filter); barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 1e34962ef..7513ee855 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -20,6 +20,9 @@ #endif #include #include +#include +#include +#include #include #include #include @@ -58,7 +61,70 @@ void require(bool condition, const char* reason) { // go. APS5_NO_RESOURCE_CACHE=1 builds every dispatch's resources as before. using ResourceCache = Graphics::ResourceCache; -void WriteFrameBmp(int index, std::uint32_t fullWidth, std::uint32_t fullHeight, std::span full); +std::uint32_t DumpScale(); +void WriteFrameBmp(int index, std::uint32_t fullWidth, std::uint32_t fullHeight, std::span full, std::uint32_t scale); + +class FrameDumpWriter { +public: + ~FrameDumpWriter() { + { + std::lock_guard lock(mutex); + stopping = true; + } + changed.notify_one(); + if (worker.joinable()) worker.join(); + } + + void Enqueue(int index, std::uint32_t width, std::uint32_t height, std::uint32_t scale, std::vector pixels) { + std::lock_guard lock(mutex); + require(!stopping, "frame dump writer has stopped"); + require(pixels.size() <= maxBytes - queuedBytes, "frame dump queue exceeded 64 MiB"); + if (!worker.joinable()) worker = std::thread([this] { run(); }); + const auto bytes = pixels.size(); + pending.push_back({index, width, height, scale, std::move(pixels)}); + queuedBytes += bytes; + changed.notify_one(); + } + +private: + struct Frame { + int index; + std::uint32_t width; + std::uint32_t height; + std::uint32_t scale; + std::vector pixels; + }; + + void run() { + try { + for (;;) { + std::deque batch; + { + std::unique_lock lock(mutex); + changed.wait(lock, [this] { return stopping || !pending.empty(); }); + if (pending.empty() && stopping) return; + batch.swap(pending); + } + for (const auto& frame : batch) { + WriteFrameBmp(frame.index, frame.width, frame.height, frame.pixels, frame.scale); + std::lock_guard lock(mutex); + queuedBytes -= frame.pixels.size(); + } + } + } catch (const std::exception& error) { + std::fprintf(stderr, "[gpu] frame dump writer failed: %s\n", error.what()); + std::terminate(); + } + } + + static constexpr std::size_t maxBytes = 64 * 1024 * 1024; + std::mutex mutex; + std::condition_variable changed; + std::deque pending; + std::thread worker; + std::size_t queuedBytes = 0; + bool stopping = false; +}; // The [present] line's counters (see the header); mutated under State::presentMutex. VulkanDevice::PresentStatistics presentCounters{}; @@ -175,6 +241,10 @@ struct VulkanDevice::State { VkCommandBuffer commands = VK_NULL_HANDLE; std::shared_ptr kept; std::unique_ptr dumpBuffer; + std::unique_ptr dumpScaler; + std::uint32_t dumpWidth = 0; + std::uint32_t dumpHeight = 0; + std::uint32_t dumpScale = 1; bool dumpRecorded = false; int dumpIndex = 0; std::uint32_t imageIndex = 0; @@ -207,7 +277,7 @@ struct VulkanDevice::State { bool queuePending = false; std::uint32_t queueIndex = 0; int nextDumpIndex = 0; - std::thread dumpWriter; + FrameDumpWriter dumpWriter; template TFunction InstanceFunction(const char* name) const { @@ -358,17 +428,14 @@ struct VulkanDevice::State { } void WriteFrameDump(const PresentSlot& slot) { - require(slot.dumpBuffer != nullptr && scaler != nullptr, "frame dump was not recorded"); - // The readback buffer is reused by the slot's next dump, so its bytes are copied out before - // the writer thread (one at a time) downscales and writes them. + require(slot.dumpBuffer != nullptr, "frame dump was not recorded"); std::vector full(slot.dumpBuffer->Bytes().begin(), slot.dumpBuffer->Bytes().end()); - if (dumpWriter.joinable()) dumpWriter.join(); const auto index = slot.dumpIndex; - const auto width = scaler->SourceWidth(); - const auto height = scaler->SourceHeight(); + const auto width = slot.dumpWidth; + const auto height = slot.dumpHeight; static const auto start = std::chrono::steady_clock::now(); std::fprintf(stderr, "[gpu] frame_%03d.bmp: %ux%u display read back on the GPU at %.1f s\n", index, width, height, std::chrono::duration(std::chrono::steady_clock::now() - start).count()); - dumpWriter = std::thread([index, width, height, pixels = std::move(full)] { WriteFrameBmp(index, width, height, pixels); }); + dumpWriter.Enqueue(index, width, height, slot.dumpScale, std::move(full)); } // Blocks on the fence, or polls it every 50 us with APS5_PRESENT_POLL_FENCE=1 (the difference @@ -402,8 +469,10 @@ struct VulkanDevice::State { } if (recorder) SettleRetired(true); } - if (dumpWriter.joinable()) dumpWriter.join(); - for (auto& slot : presentSlots) slot.dumpBuffer.reset(); + for (auto& slot : presentSlots) { + slot.dumpBuffer.reset(); + slot.dumpScaler.reset(); + } // Results still in the unit shadows reach guest memory before the device goes; the // slabs (kept by the publishing batch) are freed by the recorder's teardown. if (recorder) { @@ -1542,15 +1611,19 @@ namespace { std::uint32_t DumpScale() { static const std::uint32_t scale = [] { const char* value = std::getenv("APS5_DUMP_SCALE"); - const int parsed = value ? std::atoi(value) : 4; - return static_cast(parsed > 0 ? parsed : 1); + if (value == nullptr) return std::uint32_t{4}; + std::uint32_t parsed = 0; + const auto end = value + std::strlen(value); + const auto result = std::from_chars(value, end, parsed); + require(result.ec == std::errc{} && result.ptr == end && parsed > 0 && parsed <= 16384, "APS5_DUMP_SCALE must be between 1 and 16384"); + return parsed; }(); return scale; } // frame_.bmp: every DumpScale()-th pixel of a BGRA8 image, 32-bit top-down. -void WriteFrameBmp(int index, std::uint32_t fullWidth, std::uint32_t fullHeight, std::span full) { - const auto scale = DumpScale(); +void WriteFrameBmp(int index, std::uint32_t fullWidth, std::uint32_t fullHeight, std::span full, std::uint32_t scale) { + require(scale != 0 && full.size() == static_cast(fullWidth) * fullHeight * 4, "invalid frame dump pixels"); const std::uint32_t width = (fullWidth + scale - 1) / scale; const std::uint32_t height = (fullHeight + scale - 1) / scale; std::vector pixels(static_cast(width) * height * 4); @@ -1559,16 +1632,19 @@ void WriteFrameBmp(int index, std::uint32_t fullWidth, std::uint32_t fullHeight, } char name[32]; std::snprintf(name, sizeof(name), "frame_%03d.bmp", index); - if (std::FILE* file = std::fopen(name, "wb")) { + { + std::ofstream file; + file.exceptions(std::ios::failbit | std::ios::badbit); + file.open(name, std::ios::binary); const std::uint32_t imageBytes = width * height * 4; const std::uint32_t header[13] = {0, 0, 54, 40, width, static_cast(-static_cast(height)), 1u | (32u << 16u), 0, imageBytes, 2835, 2835, 0, 0}; const std::uint16_t magic = 0x4d42; const std::uint32_t fileBytes = 54 + imageBytes; - std::fwrite(&magic, 2, 1, file); - std::fwrite(&fileBytes, 4, 1, file); - std::fwrite(header + 1, 4, 12, file); - std::fwrite(pixels.data(), 1, pixels.size(), file); - std::fclose(file); + file.write(reinterpret_cast(&magic), 2); + file.write(reinterpret_cast(&fileBytes), 4); + file.write(reinterpret_cast(header + 1), 48); + file.write(reinterpret_cast(pixels.data()), static_cast(pixels.size())); + file.close(); } } @@ -1668,7 +1744,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { if (dumps.dumped < dumps.limit) { if (dumps.cpu) { const auto full = ReadDisplayBuffer(buffer); - WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, full); + WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, full, DumpScale()); } else { state->nextDumpIndex = dumps.dumped++; dumpFrame = true; @@ -1739,9 +1815,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu require(index < state->images.size() && index < state->rendered.size(), "acquired image index is out of range"); auto& rendered = state->rendered[index]; const bool clearOnly = pixels.empty() && display == nullptr; - // A resident image is blitted straight to the swapchain; a frame dump goes through the - // scaler's BGRA8 image so the readback has one format. - const bool direct = resident != nullptr && !dumpFrame; + const bool direct = resident != nullptr; // The scaler's source image, the color transfer's staging and the upload buffer are single // objects an in-flight blit through them may still read: the paths using or re-creating them // wait for every slot first. The game path did so before taking the mutex @@ -1848,6 +1922,15 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); if (direct) PresentationScaler::RecordBlitFrom(graphicsContext(), commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, width, height, residentFilter, barrier.image, state->extent.width, state->extent.height); else state->scaler->RecordBlit(commands, barrier.image, state->extent.width, state->extent.height); + if (dumpFrame && direct) { + const auto scale = DumpScale(); + slot.dumpWidth = (width + scale - 1) / scale; + slot.dumpHeight = (height + scale - 1) / scale; + slot.dumpScale = 1; + if (!slot.dumpScaler) slot.dumpScaler = std::make_unique(graphicsContext(), VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM); + slot.dumpScaler->EnsureSourceImage(slot.dumpWidth, slot.dumpHeight); + slot.dumpScaler->RecordBlitInto(commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, VK_FILTER_NEAREST, width, height); + } if (resident != nullptr) { // Later batches write the image again (the next frame into this buffer, a refresh): they // start after the blit has read it. @@ -1857,9 +1940,14 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); } if (dumpFrame) { - const auto dumpBytes = static_cast(state->scaler->SourceWidth()) * state->scaler->SourceHeight() * 4; + if (!direct) { + slot.dumpWidth = state->scaler->SourceWidth(); + slot.dumpHeight = state->scaler->SourceHeight(); + slot.dumpScale = DumpScale(); + } + const auto dumpBytes = static_cast(slot.dumpWidth) * slot.dumpHeight * 4; if (!slot.dumpBuffer || slot.dumpBuffer->Bytes().size() != dumpBytes) slot.dumpBuffer = std::make_unique(graphicsContext(), dumpBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); - state->scaler->RecordReadback(commands, slot.dumpBuffer->Handle()); + (direct ? slot.dumpScaler : state->scaler)->RecordReadback(commands, slot.dumpBuffer->Handle()); } } barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; @@ -1938,8 +2026,7 @@ VulkanDevice::PresentStatistics VulkanDevice::PresentCounts() { bool VulkanDevice::PresentWaitsForSlots(const DisplayBuffer* buffer) const { if (buffer == nullptr || !state->SharedSlotInFlight()) return false; if (buffer->tilingMode == 1) return true; - const auto& dumps = Dumps(); - if ((dumps.dumped < dumps.limit && !dumps.cpu) || NoResidentPresent()) return true; + if (NoResidentPresent()) return true; VkFilter filter = VK_FILTER_LINEAR; bool pending = false; if (PresentableResident(graphicsContext(), *buffer, filter, pending) == nullptr) return true; From 76f1d48e60686cfea7c924fb4340f93f3123bc7e Mon Sep 17 00:00:00 2001 From: boykopovar Date: Mon, 28 Sep 2026 13:27:14 +0300 Subject: [PATCH 15/46] debug(agc): detect gpu input buffer changes between recording and execution --- .../Execution/include/CaptureTrace.hpp | 95 +++++++++++++++++++ .../libSceAgcDriver/Execution/src/Driver.cpp | 5 + .../Execution/src/VulkanDevice.cpp | 3 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 71 +++++++++++++- .../libSceAgcDriver/Graphics/src/Recorder.cpp | 5 + .../Graphics/src/ShaderResources.cpp | 2 + .../libSceAgcDriver/Graphics/src/Texture.cpp | 6 ++ 7 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp new file mode 100644 index 000000000..56dd9215f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +class CaptureTrace { +public: + static bool Enabled() { + static const bool enabled = std::getenv("APS5_CAPTURE_TRACE") != nullptr; + return enabled; + } + + template + static void Log(const char* format, TArgs... args) { + if (!Enabled()) return; + std::array line{}; + const auto size = std::snprintf(line.data(), line.size(), format, args...); + if (size < 0 || static_cast(size) >= line.size()) throw std::runtime_error("Capture trace event exceeds 2047 bytes"); + static CaptureTrace& trace = instance(); + trace.append(std::string(line.data(), static_cast(size))); + } + +private: + static CaptureTrace& instance() { + static CaptureTrace trace; + return trace; + } + + CaptureTrace() { + output.exceptions(std::ios::badbit | std::ios::failbit); + output.open("capture-trace.log", std::ios::binary); + worker = std::thread([this] { run(); }); + } + + ~CaptureTrace() { + { + std::lock_guard lock(mutex); + stopping = true; + } + changed.notify_one(); + worker.join(); + } + + void append(std::string line) { + const auto elapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); + std::lock_guard lock(mutex); + if (pending.size() + line.size() + 64 > 16 * 1024 * 1024) throw std::runtime_error("Capture trace queue exceeded 16 MiB"); + pending += std::to_string(++sequence) + " " + std::to_string(elapsed) + "us " + line + "\n"; + if (line.starts_with("blit ")) flushRequested = true; + if (pending.size() >= 65536 || flushRequested) changed.notify_one(); + } + + void run() { + try { + for (;;) { + std::string batch; + { + std::unique_lock lock(mutex); + changed.wait_for(lock, std::chrono::milliseconds(100), [this] { return stopping || flushRequested || pending.size() >= 65536; }); + if (stopping && pending.empty()) return; + batch.swap(pending); + flushRequested = false; + } + if (batch.empty()) continue; + output.write(batch.data(), static_cast(batch.size())); + output.flush(); + } + } catch (const std::exception& error) { + std::fprintf(stderr, "Capture trace failed: %s\n", error.what()); + std::terminate(); + } + } + + std::ofstream output; + std::mutex mutex; + std::condition_variable changed; + std::string pending; + std::thread worker; + std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now(); + unsigned long long sequence = 0; + bool stopping = false; + bool flushRequested = false; +}; + +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp index 4af28bfd6..e02a1ac14 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -5621,6 +5622,7 @@ private: const auto opcode = (header >> 8u) & 0xffu; // Names this packet for the flush hook's sync attribution ([hooksync]); the flip is 0xffff. GuestMemory::SetCurrentPacket(header == FlipPacketHeader ? 0xffffu : opcode, submission.queue); + CaptureTrace::Log("packet submission=%llu queue=%x offset=%zu header=%08x words=%zu", static_cast(submission.serial), submission.queue, cursor, header, packet.size()); // Pending labels and full batches go to the GPU before this packet's own work starts // (see flushBetweenPackets); labels themselves only check the deadline, so a label // group shares one submission. Timed on its own, before the packet's timer starts. @@ -5930,6 +5932,7 @@ private: frame->IncludeSubmission(submission.serial, now, now, now, true); frame->SetFlip(submission.serial, cursor, now, now); frame->NoteFlipBatches(batchesAtFlip, unsignaledAtFlip); + CaptureTrace::Log("flip frame=%llu submission=%llu offset=%zu batch=%llu unsignaled=%llu", static_cast(frameSerial), static_cast(submission.serial), cursor, static_cast(batchesAtFlip), static_cast(unsignaledAtFlip)); submission.flips.at(cursor)->GpuReady(frame); } else if (opcode == 0x15) { timed(&WorkerProfile::dispatchMs, [&] { tolerate("dispatch", [&] { dispatch(queue, packet, submission); }); }); @@ -5994,6 +5997,7 @@ private: std::string rejected; const auto verdict = draw(queue, packet, submission, rejected); drawn = verdict == DrawVerdict::Drawn; + CaptureTrace::Log("draw submission=%llu queue=%x offset=%zu target=%llx mask=%x verdict=%d reason=%.256s", static_cast(submission.serial), submission.queue, cursor, static_cast(color), readRegister(queue.context, 0x8e), static_cast(verdict), rejected.c_str()); if (verdict == DrawVerdict::Rejected) { skipped(rejected); countSkip(Graphics::DrawSkip::Prechecked); @@ -6003,6 +6007,7 @@ private: std::fprintf(stderr, "[draw] target 0x%llx mask 0x%x ok\n",static_cast(color), readRegister(queue.context, 0x8e)); } } catch (const std::exception& error) { + CaptureTrace::Log("draw-error submission=%llu offset=%zu reason=%.256s", static_cast(submission.serial), cursor, error.what()); skipped(error.what()); countSkip(Graphics::DrawSkip::Thrown); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 7513ee855..83bd0b373 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "BdaAbi.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" @@ -1755,6 +1756,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { lastReport = std::chrono::steady_clock::now(); std::fprintf(stderr, "[flip] %llu presents from the resident image (%llu refreshed first), through guest memory: %llu not pending, %llu unsuitable; %llu GPU frame dumps\n", static_cast(residentPresents), static_cast(refreshedPresents), static_cast(notPending), static_cast(unsuitable), static_cast(gpuDumps)); } + CaptureTrace::Log("present dump=%d address=%llx width=%u height=%u resident=%d generation=%llu", dumpFrame ? state->nextDumpIndex : -1, static_cast(buffer.address), buffer.width, buffer.height, resident != nullptr, static_cast(resident ? resident->Generation() : 0)); if (!present(buffer.width, buffer.height, true, {}, &buffer, resident, filter, dumpFrame)) { // A dropped frame (swapchain out of date) keeps the dump numbering contiguous. if (dumpFrame) --dumps.dumped; @@ -1837,6 +1839,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu SubmitRecorded(false); std::chrono::steady_clock::time_point lastSubmittedAt{}; const std::uint64_t batchesAtBlit = state->recorder ? state->recorder->NewestSubmitted(&lastSubmittedAt) : 0; + CaptureTrace::Log("blit dump=%d batch=%llu slot=%zu image=%u direct=%d", dumpFrame ? state->nextDumpIndex : -1, static_cast(batchesAtBlit), state->presentCursor, index, direct); timing.Mark("pixel_upload"); APS5_LOG_OUT_DEBUG("present source=%s bytes=%zu", pixels.empty() ? "clear" : "pixels", pixels.size()); auto commands = slot.commands; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index b90d4969a..3fd283069 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -8,6 +8,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libc/include/General.hpp" #include "Optimization/include/Optimization/ShaderStageInputInfo.hpp" @@ -998,6 +999,72 @@ struct RecordedDraw { // attachments with nothing recorded between them share one pass; the host-write and host-read // barriers are implicit in the submission and the fence. Then the kept objects, the completion // work and, for a waited-for draw, the recorder sync. +bool CaptureInputsEnabled() { + static const bool enabled = std::getenv("APS5_CAPTURE_INPUTS") != nullptr; + Require(!enabled || CaptureTrace::Enabled(), "APS5_CAPTURE_INPUTS requires APS5_CAPTURE_TRACE"); + return enabled; +} + +void captureInputs(const Context& context, Recorder& recorder, VkCommandBuffer commands, const ShaderResources& resources, std::uint64_t target) { + struct Sample { + std::uint64_t address; + std::size_t offset; + std::vector expected; + VkBuffer source; + VkDeviceSize sourceOffset; + }; + static unsigned long long nextDraw = 0; + const auto draw = ++nextDraw; + const auto batch = static_cast(recorder.Submissions() + 1); + std::vector samples; + std::size_t total = 0; + for (const auto& [begin, end] : resources.InPlaceReads()) { + Require(end >= begin, "invalid capture input range"); + const auto bytes = static_cast(end - begin); + if (bytes == 0 || bytes > 512) continue; + if (resources.WritesOverlap(begin, bytes) || recorder.PendingWriteOverlaps(begin, bytes)) { + CaptureTrace::Log("input-skip draw=%llu batch=%llu address=%llx bytes=%zu reason=gpu-writer", draw, batch, static_cast(begin), bytes); + continue; + } + const auto* imported = HostImportFor(context, begin, bytes); + Require(imported != nullptr, "capture input has no host import"); + Require(begin % 4 == 0 && bytes % 4 == 0, "capture input is not aligned for a Vulkan buffer copy"); + Require(total + bytes <= 65536, "capture inputs exceed 64 KiB per draw"); + Sample sample{begin, total, std::vector(bytes), imported->buffer, begin - imported->base}; + std::memcpy(sample.expected.data(), reinterpret_cast(begin), bytes); + total += bytes; + samples.push_back(std::move(sample)); + } + CaptureTrace::Log("input-capture draw=%llu batch=%llu target=%llx ranges=%zu bytes=%zu", draw, batch, static_cast(target), samples.size(), total); + if (samples.empty()) return; + auto readback = std::make_shared(context, total, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + const auto copy = context.Function("vkCmdCopyBuffer"); + for (const auto& sample : samples) { + const VkBufferCopy region{sample.sourceOffset, sample.offset, sample.expected.size()}; + copy(commands, sample.source, readback->Handle(), 1, ®ion); + } + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_HOST_READ_BIT | VK_ACCESS_SHADER_READ_BIT); + recorder.OnComplete([draw, batch, target, readback, samples = std::move(samples)] { + const auto actual = readback->Bytes(); + std::size_t differences = 0; + for (const auto& sample : samples) { + std::size_t changed = 0; + for (std::size_t index = 0; index < sample.expected.size(); ++index) { + const auto gpu = actual[sample.offset + index]; + if (sample.expected[index] == gpu) continue; + if (changed < 16) CaptureTrace::Log("input-byte draw=%llu batch=%llu address=%llx offset=%zu cpu=%02x gpu=%02x", draw, batch, static_cast(sample.address), index, std::to_integer(sample.expected[index]), std::to_integer(gpu)); + ++changed; + } + if (changed != 0) { + ++differences; + CaptureTrace::Log("input-mismatch draw=%llu batch=%llu target=%llx address=%llx bytes=%zu changed=%zu", draw, batch, static_cast(target), static_cast(sample.address), sample.expected.size(), changed); + } + } + CaptureTrace::Log("input-result draw=%llu batch=%llu ranges=%zu mismatched=%zu", draw, batch, samples.size(), differences); + }); +} + void recordDraw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span shaders, DrawInputs& inputs, RecordedDraw& record, DrawOutcome& outcome, DrawTimer& timer, double& ownWaitedMs) { auto* recorder = record.recorder; auto& resources = *record.resources; @@ -1030,10 +1097,12 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam if (recorder->HasQueuedKeyStores() && (resources.HoldsLease() || recorder->AnyQueuedKeyStore(touches))) recorder->FlushKeyStores(); // A queued label store over such memory likewise (Recorder::RecordStore). if (recorder->HasQueuedStores() && (resources.HoldsLease() || recorder->AnyQueuedStore(touches))) recorder->FlushStores(); - const bool continued = !readsTarget && !gpuIndirect && recorder->ContinuesRenderPass(passKey); + const bool capture = CaptureInputsEnabled(); + const bool continued = !capture && !readsTarget && !gpuIndirect && recorder->ContinuesRenderPass(passKey); outcome.passContinued = continued; outcome.passBegun = !continued; const auto commands = continued ? recorder->CommandsInRenderPass() : recorder->Commands(); + if (capture) captureInputs(context, *recorder, commands, resources, record.targets.empty() || record.targets.front() == nullptr ? 0 : record.targets.front()->Descriptor().baseAddress); // The draw's [gputime] class range: from its first barrier to the pass's trailing barrier (a // continued draw lies inside its pass's range). const auto drawTiming = !continued ? recorder->BeginGpuTiming(CommandClass::Draw) : Recorder::NoTiming; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index d9184240f..f796220fd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" @@ -1894,6 +1895,7 @@ void Recorder::OnComplete(std::function action) { } bool Recorder::noteWrite(std::uint64_t address, std::size_t bytes, bool ownLabel) { + CaptureTrace::Log("buffer-write batch=%llu address=%llx bytes=%zu label=%d", static_cast(submissions + 1), static_cast(address), bytes, ownLabel); if (bytes == 0) return false; ensureOpen(); const auto end = address + bytes; @@ -2015,6 +2017,7 @@ bool Recorder::ReadTracking() { } void Recorder::NotePendingRead(std::uint64_t address, std::size_t bytes, ReadKind kind) { + CaptureTrace::Log("buffer-read batch=%llu address=%llx bytes=%zu kind=%d", static_cast(submissions + 1), static_cast(address), bytes, static_cast(kind)); if (bytes == 0 || !ReadTrackingEnabled()) return; ensureOpen(); open->reads.push_back({address, address + bytes, kind}); @@ -2025,6 +2028,7 @@ void Recorder::NotePendingReads(std::span(submissions + 1), static_cast(begin), static_cast(end - begin), static_cast(kind)); if (end > begin) open->reads.push_back({begin, end, kind}); } readsNoted.fetch_add(ranges.size(), std::memory_order_relaxed); @@ -2345,6 +2349,7 @@ void Recorder::Submit() { submission.pCommandBuffers = &batch->commands; // The timeline reaches this batch's serial when it completes (see WaitSerial). const std::uint64_t serial = submissions + 1; + CaptureTrace::Log("submit batch=%llu reads=%zu writes=%zu", static_cast(serial), batch->reads.size(), batch->writes.size()); VkTimelineSemaphoreSubmitInfoKHR timelineInfo{VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO_KHR}; timelineInfo.signalSemaphoreValueCount = 1; timelineInfo.pSignalSemaphoreValues = &serial; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index f55e3ebf6..35c605c26 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include @@ -260,6 +261,7 @@ std::shared_ptr sampledStorageSource(const Context& context, con // `guestBytes` is the surface size when the caller described the surface already (0: described here). std::shared_ptr cachedTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components, std::uint64_t guestBytes = 0) { + CaptureTrace::Log("sampled-lookup address=%llx width=%u height=%u dcc=%llx", static_cast(resource.baseAddress), resource.width, resource.height, static_cast(resource.dccAddress)); static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; const bool profile = LookupOutcomes::Profiled(); const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index da0d220ee..74f55c1a8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" @@ -1062,6 +1063,7 @@ bool StorageTexture::Refresh() { if (profile) LookupOutcomes::Add(stamped ? LookupOutcomes::RefreshUnchanged : LookupOutcomes::RefreshCompared, start); return true; } + CaptureTrace::Log("refresh image=%llx bytes=%llu generation=%llu dirty=%d keysChanged=%d cpuWrote=%d", static_cast(descriptor.baseAddress), static_cast(guestBytes), static_cast(generation), dirty, keysChanged, cpuWrote); // Debug aid: APS5_TRACE_UPLOAD names why a storage image is uploaded again. static const bool traceUpload = std::getenv("APS5_TRACE_UPLOAD") != nullptr; if (traceUpload) { @@ -1115,6 +1117,7 @@ bool StorageTexture::Refresh() { if (changed[layer]) { pendingResults = true; if (blockUnits && droppable(layer)) { + CaptureTrace::Log("drop-unit image=%llx unit=%u generation=%llu stamped=%d keys=%d", static_cast(descriptor.baseAddress), layer, static_cast(layerGeneration[layer]), layer < stampedBlocks.size() && stampedBlocks[layer] != 0, static_cast(keys)); layerPending[layer] = false; dropped = true; continue; @@ -1167,6 +1170,7 @@ void traceKeyStore(const char* path, const GuestTextureResource& descriptor, std } void StorageTexture::upload(const std::vector* layers) { + CaptureTrace::Log("upload image=%llx bytes=%llu generation=%llu reason=%s partial=%d", static_cast(descriptor.baseAddress), static_cast(guestBytes), static_cast(generation), uploadReason, layers != nullptr); const bool profile = LookupOutcomes::Profiled(); const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; const auto elementBytes = BytesPerElement(descriptor.format); @@ -1812,6 +1816,7 @@ void StorageTexture::refreshGeneration() { } void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) { + CaptureTrace::Log("image-write image=%llx first=%u count=%u generation=%llu", static_cast(descriptor.baseAddress), first, count, static_cast(generation)); static const bool eager = std::getenv("APS5_EAGER_WRITEBACK") != nullptr || std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; for (std::uint32_t layer = first; layer < first + count; ++layer) layerPending[layer] = true; // Results of this image now cover the alias's results borrowed into these units: this image @@ -2818,6 +2823,7 @@ void StorageTexture::writeBack(std::uint64_t address, std::size_t bytes) { } void StorageTexture::writeBackLayers(const std::vector& layers) { + CaptureTrace::Log("writeback image=%llx generation=%llu reason=%s units=%zu", static_cast(descriptor.baseAddress), static_cast(generation), flushReason, static_cast(std::count(layers.begin(), layers.end(), true))); static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; PhaseTimer timer; struct Account { From 2e8c1096fdb43267bbd801b58bf6730ec23ae3df Mon Sep 17 00:00:00 2001 From: OverkillLabs <10800190+OverkillLabs@users.noreply.github.com> Date: Mon, 28 Sep 2026 12:45:15 +0300 Subject: [PATCH 16/46] feat(libSceMsgDialog): real dialog state machine, auto-answer without UI Replace the unimplemented stubs with a working NONE -> INITIALIZED -> RUNNING -> FINISHED state machine for the message dialog PRX. Open() best-effort parses the SceMsgDialogParam layout to pick a sensible default button (auto-focus-no variants answer No, everything else answers Yes/OK), then auto-finishes the dialog after a couple of status polls so a title that waits on a dialog for user input never blocks forever. Progress-bar mode is instead closed by the title itself via sceMsgDialogClose, with a time cap as a safety net. GetResult now fills in a real SceMsgDialogResult (mode/result/buttonId) instead of leaving the caller's buffer untouched. --- core/libs/HitLog.hpp | 18 +++ core/libs/prx/libSceMsgDialog/Export.cpp | 195 ++++++++++++++++++----- 2 files changed, 169 insertions(+), 44 deletions(-) create mode 100644 core/libs/HitLog.hpp diff --git a/core/libs/HitLog.hpp b/core/libs/HitLog.hpp new file mode 100644 index 000000000..e94e3228b --- /dev/null +++ b/core/libs/HitLog.hpp @@ -0,0 +1,18 @@ +#ifndef CORE_LIBS_HITLOG_HPP +#define CORE_LIBS_HITLOG_HPP + +#include +#include + +// Logs the first three calls of the surrounding call site, then stays silent (keeps guest logs usable). +#define APS5_HIT(tag, ...) \ + do { \ + static std::atomic aps5HitCount{0}; \ + if (aps5HitCount.fetch_add(1, std::memory_order_relaxed) < 3) { \ + std::fprintf(stderr, "[" tag "] " __VA_ARGS__); \ + std::fputc('\n', stderr); \ + std::fflush(stderr); \ + } \ + } while (0) + +#endif diff --git a/core/libs/prx/libSceMsgDialog/Export.cpp b/core/libs/prx/libSceMsgDialog/Export.cpp index f826fe36a..1e02b26ea 100644 --- a/core/libs/prx/libSceMsgDialog/Export.cpp +++ b/core/libs/prx/libSceMsgDialog/Export.cpp @@ -1,66 +1,173 @@ +// libSceMsgDialog: message dialog state machine (NONE -> INITIALIZED -> RUNNING -> FINISHED) without a visible dialog. +// The dialog "closes" with the affirmative button after a couple of status polls, so a title waiting for the user +// never blocks. The message text is logged for the first calls. +#include #include +#include #include +#include +#include +#include + #include "SceTypes.hpp" +#include "HitLog.hpp" #include "prx/libc/include/General.hpp" +#ifdef _WIN32 +#include +#endif + +namespace { +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; +constexpr int kStatusRunning = 2; +constexpr int kStatusFinished = 3; + +constexpr int kErrOk = 0; +constexpr int kErrNotInitialized = static_cast(0x80B80003u); +constexpr int kErrNotFinished = static_cast(0x80B80005u); +constexpr int kErrBusy = static_cast(0x80B80007u); +constexpr int kErrNotRunning = static_cast(0x80B8000Bu); +constexpr int kErrArgNull = static_cast(0x80B8000Du); + +constexpr int kModeUserMsg = 1; +constexpr int kModeProgressBar = 2; +constexpr int kModeSystemMsg = 3; + +constexpr int kButtonTypeOk = 0; +constexpr int kButtonTypeYesNo = 1; +constexpr int kButtonTypeNone = 2; +constexpr int kButtonTypeOkCancel = 3; +constexpr int kButtonTypeYesNoFocusNo = 4; + +constexpr int kButtonIdOk = 1; // OK / YES / BUTTON1 +constexpr int kButtonIdNo = 2; // NO / BUTTON2 +constexpr int kResultOk = 0; + +std::mutex g_lock; +int g_status = kStatusNone; +int g_mode = kModeUserMsg; +int g_buttonId = kButtonIdOk; +int g_pollsLeft = 0; +std::chrono::steady_clock::time_point g_openedAt; + +bool Readable(const void* p, std::size_t n) { +#ifdef _WIN32 + if (p == nullptr) return false; + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery(p, &mbi, sizeof(mbi)) == 0 || mbi.State != MEM_COMMIT) return false; + if ((mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) return false; + const auto end = reinterpret_cast(mbi.BaseAddress) + mbi.RegionSize; + return reinterpret_cast(p) + n <= end; +#else + return p != nullptr; +#endif +} + +// Best-effort inspection of SceMsgDialogParam: { SceCommonDialogBaseParam base (0x30); size_t size; int mode (0x38); +// UserMessageParam* userMsg (0x40); ProgressBarParam* progress (0x48); SystemMessageParam* system (0x50); ... }. +void Inspect(const void* param) { + const std::uint8_t* bytes = static_cast(param); + g_mode = kModeUserMsg; + g_buttonId = kButtonIdOk; + if (!Readable(bytes, 0x60)) return; + std::int32_t mode = 0; + std::memcpy(&mode, bytes + 0x38, 4); + if (mode >= kModeUserMsg && mode <= kModeSystemMsg) g_mode = mode; + if (g_mode != kModeUserMsg) return; + const std::uint8_t* userMsg = nullptr; + std::memcpy(&userMsg, bytes + 0x40, 8); + if (!Readable(userMsg, 16)) return; + std::int32_t buttonType = 0; + std::memcpy(&buttonType, userMsg, 4); + const char* msg = nullptr; + std::memcpy(&msg, userMsg + 8, 8); + if (buttonType == kButtonTypeYesNoFocusNo) g_buttonId = kButtonIdNo; + if (Readable(msg, 4)) { + char text[161] = {}; + for (int i = 0; i < 160 && Readable(msg + i, 1) && msg[i] != '\0'; ++i) text[i] = (msg[i] >= 0x20 || msg[i] == '\n') ? msg[i] : '?'; + APS5_HIT("MSGDIALOG", "open: buttons=%d text=\"%s\" -> auto answer", buttonType, text); + } +} + +int Poll() { + std::lock_guard g(g_lock); + if (g_status == kStatusRunning) { + bool done = false; + if (g_mode == kModeProgressBar) { + // Progress dialogs are ended by the title (sceMsgDialogClose); the cap only prevents a wait without end. + done = std::chrono::steady_clock::now() - g_openedAt > std::chrono::seconds(30); + } else { + done = --g_pollsLeft <= 0; + } + if (done) g_status = kStatusFinished; + } + return g_status; +} +} // namespace + extern "C" { -int APS5_VABI sceMsgDialogClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceMsgDialogGetResult(void* result) { - (void)result; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceMsgDialogGetStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceMsgDialogInitialize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard g(g_lock); + if (g_status == kStatusNone) g_status = kStatusInitialized; + APS5_HIT("MSGDIALOG", "sceMsgDialogInitialize"); + return kErrOk; +} +int APS5_VABI sceMsgDialogTerminate(void) { + std::lock_guard g(g_lock); + const bool was = g_status != kStatusNone; + g_status = kStatusNone; + return was ? kErrOk : kErrNotInitialized; } - int APS5_VABI sceMsgDialogOpen(const void* param) { - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; + if (param == nullptr) return kErrArgNull; + std::lock_guard g(g_lock); + if (g_status == kStatusRunning) return kErrBusy; + // A title that skipped sceMsgDialogInitialize still gets its dialog (and its FINISHED status). + Inspect(param); + g_pollsLeft = 2; + g_openedAt = std::chrono::steady_clock::now(); + g_status = kStatusRunning; + return kErrOk; } - -int APS5_VABI sceMsgDialogProgressBarInc(int target, uint32_t delta) { +int APS5_VABI sceMsgDialogGetStatus(void) { + return Poll(); +} +int APS5_VABI sceMsgDialogUpdateStatus(void) { + return Poll(); +} +// SceMsgDialogResult: { int32 mode; int32 result; int32 buttonId; uint8 reserved[32] }. +int APS5_VABI sceMsgDialogGetResult(void* result) { + if (result == nullptr) return kErrArgNull; + std::lock_guard g(g_lock); + if (g_status != kStatusFinished) return kErrNotFinished; + std::int32_t out[3] = {g_mode, kResultOk, g_buttonId}; + std::memcpy(result, out, sizeof(out)); + return kErrOk; +} +int APS5_VABI sceMsgDialogClose(void) { + std::lock_guard g(g_lock); + if (g_status != kStatusRunning) return kErrNotRunning; + g_status = kStatusFinished; + return kErrOk; +} +int APS5_VABI sceMsgDialogProgressBarInc(int target, std::uint32_t delta) { (void)target; (void)delta; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard g(g_lock); + return g_status == kStatusRunning ? kErrOk : kErrNotRunning; } - int APS5_VABI sceMsgDialogProgressBarSetMsg(int target, const char* msg) { (void)target; (void)msg; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard g(g_lock); + return g_status == kStatusRunning ? kErrOk : kErrNotRunning; } - -int APS5_VABI sceMsgDialogProgressBarSetValue(int target, uint32_t rate) { +int APS5_VABI sceMsgDialogProgressBarSetValue(int target, std::uint32_t rate) { (void)target; (void)rate; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard g(g_lock); + return g_status == kStatusRunning ? kErrOk : kErrNotRunning; } - -int APS5_VABI sceMsgDialogTerminate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceMsgDialogUpdateStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } From e629f3748174fc83e315d0466ace2d6ca7b42370 Mon Sep 17 00:00:00 2001 From: OverkillLabs <10800190+OverkillLabs@users.noreply.github.com> Date: Mon, 28 Sep 2026 12:59:54 +0300 Subject: [PATCH 17/46] feat(libSceAvPlayer): implement full AvPlayer state machine Replace the stub AvPlayer export set with a general-purpose player state machine covering the full API surface: sceAvPlayerInit/InitEx/PostInit, AddSource/AddSourceEx, Start/StartEx, Pause/Resume/Stop/Close, IsActive, CurrentTime, JumpToTime, SetLooping, SetAvSyncMode, SetTrickSpeed, SetAvailableBandwidth, StreamCount/EnableStream/DisableStream/ChangeStream/ GetStreamInfo(Ex), and GetVideoData(Ex)/GetAudioData. No real decoder is wired up: the player tracks lifecycle and timing state and serves a fixed-length blank clip on a wall-clock media clock, firing the READY/PLAY/PAUSE/STOP event callbacks a title expects from a real playback session. This is enough for titles that gate progression on AvPlayer's polling/event surface rather than on decoded frame content. The non-native libSceAvPlayer/Export.cpp now forwards to the native implementation via #include rather than carrying its own duplicate stub definitions, consistent with how other libSceX/libSceX.native pairs in this tree share one implementation. --- core/libs/HitLog.hpp | 18 + .../libs/prx/libSceAvPlayer.native/Export.cpp | 773 ++++++++++++++---- core/libs/prx/libSceAvPlayer/Export.cpp | 178 +--- 3 files changed, 624 insertions(+), 345 deletions(-) create mode 100644 core/libs/HitLog.hpp diff --git a/core/libs/HitLog.hpp b/core/libs/HitLog.hpp new file mode 100644 index 000000000..e94e3228b --- /dev/null +++ b/core/libs/HitLog.hpp @@ -0,0 +1,18 @@ +#ifndef CORE_LIBS_HITLOG_HPP +#define CORE_LIBS_HITLOG_HPP + +#include +#include + +// Logs the first three calls of the surrounding call site, then stays silent (keeps guest logs usable). +#define APS5_HIT(tag, ...) \ + do { \ + static std::atomic aps5HitCount{0}; \ + if (aps5HitCount.fetch_add(1, std::memory_order_relaxed) < 3) { \ + std::fprintf(stderr, "[" tag "] " __VA_ARGS__); \ + std::fputc('\n', stderr); \ + std::fflush(stderr); \ + } \ + } while (0) + +#endif diff --git a/core/libs/prx/libSceAvPlayer.native/Export.cpp b/core/libs/prx/libSceAvPlayer.native/Export.cpp index 2c2e5bb90..bc3573e05 100644 --- a/core/libs/prx/libSceAvPlayer.native/Export.cpp +++ b/core/libs/prx/libSceAvPlayer.native/Export.cpp @@ -1,194 +1,631 @@ +// libSceAvPlayer: player lifecycle without media decoding. A source is accepted, the event callback sees the usual +// READY / PLAY transitions, and the video getters hand out frames of a fixed-length blank clip on a wall-clock +// media clock before the stream reports as finished (STOP). The frame budget matters: a title whose intro sequence +// is driven by the frame getters stops asking only once it has been given frames and then an end-of-stream, so +// answering every poll with "no frame" strands it in its own wait loop. No frame is ever decoded, so the picture +// is black. +#include +#include #include +#include #include +#include +#include +#include +#include +#include +#include + #include "SceTypes.hpp" +#include "HitLog.hpp" #include "prx/libc/include/General.hpp" +namespace { +constexpr std::int32_t kEventStop = 0x01; +constexpr std::int32_t kEventReady = 0x02; +constexpr std::int32_t kEventPlay = 0x03; +constexpr std::int32_t kEventPause = 0x04; +constexpr std::int32_t kEventWarning = 0x20; +constexpr std::int32_t kErrInvalidParams = static_cast(0x806a0001u); +constexpr std::int32_t kErrOperationFailed = static_cast(0x806a0002u); +constexpr std::uint32_t kWarningJumpComplete = 0x806a00a3u; +constexpr std::uint64_t kMagic = 0x5041564c50594c41ull; + +// The blank clip this player "plays". Both the stream info and every delivered frame are described by these +// values, so the title's own texture sizing and the surface it is handed cannot disagree. +constexpr std::uint32_t kFrameWidth = 1920; +constexpr std::uint32_t kFrameHeight = 1080; +// NV12 rows are pitched to 256 bytes; SceAvPlayerVideoEx::width publishes that coded extent, not the visible +// one, with the difference reported through cropRightOffset. +constexpr std::uint32_t kFramePitch = (kFrameWidth + 255u) & ~255u; +constexpr std::size_t kFrameBytes = static_cast(kFramePitch) * kFrameHeight * 3u / 2u; +// Limited-range black, not zero: Y=U=V=0 converts to mid green in both BT.601 and BT.709, and a title that +// samples the coded extent would read the pitch padding as a green stripe down the right edge. +constexpr std::uint8_t kBlackLuma = 0x10; +constexpr std::uint8_t kBlackChroma = 0x80; +constexpr std::uint64_t kFrameIntervalMs = 33; +constexpr std::uint64_t kFrameBudget = 120; +constexpr std::uint64_t kStreamDurationMs = kFrameBudget * kFrameIntervalMs; +constexpr double kFrameRate = 1000.0 / static_cast(kFrameIntervalMs); + +using EventCallback = void (APS5_VABI *)(void*, std::int32_t, std::int32_t, void*); +using TextureAllocator = void* (APS5_VABI *)(void*, std::uint32_t, std::uint32_t); +using TextureFree = void (APS5_VABI *)(void*, void*); + +// SceAvPlayerVideoEx. The offsets are asserted below because the guest reads them by position, not by name. +struct VideoDetailsEx { + std::uint32_t width; + std::uint32_t height; + float aspectRatio; + std::uint8_t language[4]; + std::uint8_t reserved0[4]; + std::uint32_t cropLeftOffset; + std::uint32_t cropRightOffset; + std::uint32_t cropTopOffset; + std::uint32_t cropBottomOffset; + std::uint32_t pitch; + std::uint8_t lumaBitDepth; + std::uint8_t chromaBitDepth; + std::uint8_t fullRange; + std::uint8_t reserved1[5]; + double frameRate; + std::uint32_t colourPrimaries; + std::uint32_t transferCharacteristics; + std::uint8_t reserved2[16]; +}; +static_assert(sizeof(VideoDetailsEx) == 80, "SceAvPlayerVideoEx ABI"); +static_assert(offsetof(VideoDetailsEx, cropLeftOffset) == 20, "SceAvPlayerVideoEx ABI"); +static_assert(offsetof(VideoDetailsEx, pitch) == 36, "SceAvPlayerVideoEx ABI"); +static_assert(offsetof(VideoDetailsEx, frameRate) == 48, "SceAvPlayerVideoEx ABI"); + +// SceAvPlayerFrameInfo / SceAvPlayerFrameInfoEx: the out-param of the video getters. The Ex variant carries +// the wide details union; both put their timestamp at 16 and their details at 24. +struct FrameInfo { + void* data; + std::uint8_t reserved[4]; + std::uint64_t timestamp; + std::uint32_t width; + std::uint32_t height; + float aspectRatio; + std::uint8_t language[4]; +}; +struct FrameInfoEx { + void* data; + std::uint8_t reserved[4]; + std::uint64_t timestamp; + VideoDetailsEx video; +}; +static_assert(sizeof(FrameInfo) == 40 && offsetof(FrameInfo, timestamp) == 16, "SceAvPlayerFrameInfo ABI"); +static_assert(sizeof(FrameInfoEx) == 104 && offsetof(FrameInfoEx, video) == 24, "SceAvPlayerFrameInfoEx ABI"); + +// Prefix of sceAvPlayerInitData / sceAvPlayerInitDataEx that this player reads. The Ex layout starts with its own size. +struct InitPrefix { + void* memObject; + void* alloc; + void* dealloc; + void* allocTexture; + void* deallocTexture; + void* fileObject; + void* fileOpen; + void* fileClose; + void* fileRead; + void* fileSize; + void* eventObject; + void* eventCallback; + std::int32_t debugLevel; + std::uint32_t basePriority; + std::int32_t numFramebuffers; + std::uint8_t autoStart; +}; + +struct Player { + std::uint64_t magic = kMagic; + void* eventObject = nullptr; + EventCallback callback = nullptr; + void* memObject = nullptr; + TextureAllocator allocTexture = nullptr; + TextureFree deallocTexture = nullptr; + bool autoStart = false; + bool haveSource = false; + bool playing = false; + bool paused = false; + bool stopFired = false; + bool looping = false; + bool noStorage = false; + std::int32_t numFramebuffers = 0; + // Media clock. A hardware decoder advances the stream on its own clock and a poll picks up whichever frame + // is due; counting polls instead stretched this 4-second blank clip over 80 seconds of a 1.5 fps run, + // because the title polls exactly once per host frame. bankedMs holds the media time reached at clockAnchor, + // and a cleared anchor means the clock is stopped (paused or not yet started) or re-anchors on next read. + std::chrono::steady_clock::time_point clockAnchor{}; + std::uint64_t bankedMs = 0; + std::uint64_t framesDelivered = 0; + std::string path; + std::vector frames; + std::mutex lock; +}; + +Player* Check(void* handle) { + Player* p = static_cast(handle); + return p != nullptr && p->magic == kMagic ? p : nullptr; +} + +// The callback runs guest code and may re-enter the player, so it is always invoked without the player lock held. +void Fire(Player* p, std::int32_t event, void* data = nullptr) { + if (p->callback == nullptr) return; + // Budgeted per event number rather than per call site: one shared APS5_HIT line would cap the whole + // census at three events of any kind, so the READY/PLAY pair that opens a movie eats the budget and the + // STOP that ends it is never reported. + static std::atomic fired[64]; + const auto slot = static_cast(event) < 64 ? static_cast(event) : 0u; + if (fired[slot].fetch_add(1) < 3) + std::fprintf(stderr, "[AVP] event 0x%02x\n", event); + p->callback(p->eventObject, event, 0, data); +} + +// Caller holds the lock. +std::uint64_t MediaTime(Player& p) { + const auto now = std::chrono::steady_clock::now(); + if (p.clockAnchor == std::chrono::steady_clock::time_point{}) { + if (!p.playing || p.paused) return p.bankedMs; + p.clockAnchor = now; + } + const auto elapsed = static_cast( + std::chrono::duration_cast(now - p.clockAnchor).count()); + return p.bankedMs + elapsed; +} + +// Frame due at the current media time, saturating at the end of the clip. +std::uint64_t CurrentFrame(Player& p) { + const auto frame = MediaTime(p) / kFrameIntervalMs; + return frame > kFrameBudget ? kFrameBudget : frame; +} + +// Bank the clock where it stands and stop it. +void HaltClock(Player& p) { + p.bankedMs = MediaTime(p); + p.clockAnchor = {}; +} + +// True once the blank clip has run out -- or was never going to run, because the title gave us no allocator to +// put its frames in. Both are the same thing to a polling title: no more frames will ever arrive. +bool StreamExhausted(Player& p) { return p.noStorage || CurrentFrame(p) >= kFrameBudget; } + +// There is no decoder, so this player "plays" a fixed-length blank clip instead of the source. On real hardware +// the end of the stream is reported by the poll that finds no more frames, and the STOP event fires on that same +// poll -- which is the only end-of-movie signal a title gets if it pumps the data getters instead of +// sceAvPlayerIsActive. A title with an intro-logo or splash-video sequence commonly does exactly that, so STOP +// has to come from the poll, and only once the clip has actually run out: a title that is waiting for frames +// ends its sequence on the end-of-stream that follows them, not on one that arrives before the first. +// Returns true when this call is the one that ends the stream, so the caller fires the event outside the lock. +bool EndStream(Player* p) { + std::lock_guard g(p->lock); + if (!p->playing || p->paused || p->stopFired) return false; + if (!StreamExhausted(*p)) return false; + // Once per player, and outside the APS5_HIT budget: this is the line that says whether a movie was + // allowed to finish or is stranded in a pause that never resumed. + std::fprintf(stderr, "[AVP] end of stream: %llu frames delivered, clock at %llu ms\n", + static_cast(CurrentFrame(*p)), + static_cast(MediaTime(*p))); + std::fflush(stderr); + HaltClock(*p); + p->playing = false; + p->stopFired = true; + p->haveSource = false; + return true; +} + +// The frame buffers come from the title's own texture allocator, exactly as on hardware: SceAvPlayerInitData +// hands it out alongside the memory pool precisely so frame storage lives in graphics memory, which is what lets +// the upload the title later issues from the delivered pointer be validated as guest memory. A player that +// supplies no such allocator has nowhere to put a frame, and that is reported once -- as an exhausted stream -- +// rather than retried on every poll. Caller holds the lock. +bool PrepareFrames(Player& p) { + if (!p.frames.empty() || p.noStorage) return !p.frames.empty(); + if (p.allocTexture == nullptr || p.memObject == nullptr) { + p.noStorage = true; + APS5_HIT("AVP", "no guest frame allocator: InitData supplied no texture allocator"); + return false; + } + unsigned requested = p.numFramebuffers > 0 ? static_cast(p.numFramebuffers) : 2u; + if (requested < 2u) requested = 2u; + if (requested > 8u) requested = 8u; + const auto planeBytes = static_cast(kFramePitch) * kFrameHeight; + for (unsigned i = 0; i < requested; ++i) { + auto* frame = static_cast( + p.allocTexture(p.memObject, 0x100, static_cast(kFrameBytes))); + if (frame == nullptr) break; + std::memset(frame, kBlackLuma, planeBytes); + std::memset(frame + planeBytes, kBlackChroma, kFrameBytes - planeBytes); + p.frames.push_back(frame); + } + if (p.frames.empty()) { + p.noStorage = true; + return false; + } + std::fprintf(stderr, "[AVP] blank clip: buffers=%zu bytes=%llu pitch=%u allocator=%p first=%p\n", + p.frames.size(), static_cast(kFrameBytes), kFramePitch, + reinterpret_cast(p.allocTexture), static_cast(p.frames.front())); + std::fflush(stderr); + return true; +} + +// Publishes whichever frame the media clock has reached. Returns false when the clip is over, which is the poll +// that reports the end of the stream. Every frame of this clip is the same blank image, so skipping ahead on a +// slow host costs a title nothing it can observe -- unlike a real movie, where the clock would have to be +// slowed to the host's pace instead. Caller holds the lock. +bool DeliverFrame(Player& p, void*& data, std::uint64_t& timestamp) { + if (!p.playing || p.paused || !PrepareFrames(p)) return false; + const auto frame = CurrentFrame(p); + if (frame >= kFrameBudget) return false; + data = p.frames[frame % p.frames.size()]; + timestamp = frame * kFrameIntervalMs; + if (frame + 1 > p.framesDelivered) { + const auto previous = p.framesDelivered; + p.framesDelivered = frame + 1; + // Every thirtieth frame *crossed*, not every thirtieth index: on a slow host the clock jumps + // straight over exact multiples, and an equality test there reports nothing at all. + if (previous / 30 != p.framesDelivered / 30 || p.framesDelivered == kFrameBudget) + std::fprintf(stderr, "[AVP] blank frame #%llu of %llu t=%llu ms\n", + static_cast(p.framesDelivered), + static_cast(kFrameBudget), + static_cast(timestamp)); + } + return true; +} + +// The wide video description of the blank clip. `width` publishes the coded extent (the pitch) and the +// difference from the visible picture goes out through cropRightOffset, so a title that sizes its luma texture +// from `pitch` and a title that crops `width` both end up with the visible frame. +void WriteVideoDetails(VideoDetailsEx& out) { + out = {}; + out.width = kFramePitch; + out.height = kFrameHeight; + out.aspectRatio = static_cast(kFrameWidth) / static_cast(kFrameHeight); + out.cropRightOffset = kFramePitch - kFrameWidth; + out.pitch = kFramePitch; + out.lumaBitDepth = 8; + out.chromaBitDepth = 8; + out.frameRate = kFrameRate; +} + +// Shared body of the two video getters. The frame is written through the guest's out-param in whichever of the +// two layouts the caller announced, and a poll that finds the clip spent is turned into the end-of-stream report +// on the way out -- outside the lock, because the STOP callback runs guest code. +Bool PollVideo(Player* p, void* frame_info, bool extended) { + if (frame_info == nullptr) return 0; + void* data = nullptr; + std::uint64_t timestamp = 0; + bool delivered = false; + bool wasPaused = false; + { + std::lock_guard g(p->lock); + wasPaused = p->paused; + delivered = DeliverFrame(*p, data, timestamp); + if (delivered) { + if (extended) { + FrameInfoEx out = {}; + out.data = data; + out.timestamp = timestamp; + WriteVideoDetails(out.video); + std::memcpy(frame_info, &out, sizeof(out)); + } else { + FrameInfo out = {}; + out.data = data; + out.timestamp = timestamp; + out.width = kFrameWidth; + out.height = kFrameHeight; + out.aspectRatio = static_cast(kFrameWidth) / static_cast(kFrameHeight); + std::memcpy(frame_info, &out, sizeof(out)); + } + return 1; + } + } + // Unbudgeted (unlike APS5_HIT above): every poll that finds the player paused, so the exact interleaving + // of the game's own Pause call against its own polling loop is visible instead of guessed at. + if (wasPaused) { + std::fprintf(stderr, "[AVPLIFE] poll found paused, no frame delivered\n"); + std::fflush(stderr); + } + if (EndStream(p)) Fire(p, kEventStop); + return 0; +} + +// HANDOFF13/14: HANDOFF12's still-open question was "why does the guest pause and never resume". These +// call-site logs (unbudgeted, unlike APS5_HIT -- we need every call, not just the first three) print the +// thread that called, the player pointer (multiple concurrent players would explain a stray pause on the +// wrong handle), and the media clock's state at the moment of the call. +void LogLifecycleCall(const char* site, Player* p) { + std::ostringstream tid; + tid << std::this_thread::get_id(); + std::lock_guard g(p->lock); + std::fprintf(stderr, "[AVPLIFE] %s p=%p tid=%s playing=%d paused=%d stopFired=%d haveSource=%d framesDelivered=%llu bankedMs=%llu clockRunning=%d\n", + site, static_cast(p), tid.str().c_str(), p->playing ? 1 : 0, + p->paused ? 1 : 0, p->stopFired ? 1 : 0, p->haveSource ? 1 : 0, + static_cast(p->framesDelivered), static_cast(p->bankedMs), + p->clockAnchor != std::chrono::steady_clock::time_point{} ? 1 : 0); + std::fflush(stderr); +} + +Player* CreateFrom(const InitPrefix* d) { + Player* p = new Player(); + if (d != nullptr) { + p->eventObject = d->eventObject; + p->callback = reinterpret_cast(d->eventCallback); + p->autoStart = d->autoStart != 0; + p->memObject = d->memObject; + p->allocTexture = reinterpret_cast(d->allocTexture); + p->deallocTexture = reinterpret_cast(d->deallocTexture); + p->numFramebuffers = d->numFramebuffers; + } + return p; +} + +int AddSourceCommon(Player* p, const char* path) { + if (p == nullptr || path == nullptr || *path == '\0') return kErrInvalidParams; + LogLifecycleCall("AddSource(before)", p); + bool play = false; + { + std::lock_guard g(p->lock); + p->path = path; + p->haveSource = true; + p->playing = p->autoStart; + p->paused = false; + p->stopFired = false; + p->bankedMs = 0; + p->clockAnchor = {}; + p->framesDelivered = 0; + play = p->autoStart; + } + APS5_HIT("AVP", "AddSource '%s' (no decode: %llu blank frames then end of stream)", path, + static_cast(kFrameBudget)); + Fire(p, kEventReady); + if (play) Fire(p, kEventPlay); + return 0; +} +} // namespace + extern "C" { -int APS5_VABI sceAvPlayerAddSource(AvPlayerInternal* h, const char* filename) { - (void)h; - (void)filename; - NotImplemented_nid_no_patch(__func__); +void* APS5_VABI sceAvPlayerInit(const void* init) { + APS5_HIT("AVP", "sceAvPlayerInit"); + return CreateFrom(static_cast(init)); +} +int APS5_VABI sceAvPlayerInitEx(const void* init_ex, void** handle) { + if (handle == nullptr) return kErrInvalidParams; + // sceAvPlayerInitDataEx begins with a 64-bit size, followed by the same allocator / file / event blocks. + const std::uint8_t* base = static_cast(init_ex); + *handle = CreateFrom(base != nullptr ? reinterpret_cast(base + 8) : nullptr); return 0; } - -int APS5_VABI sceAvPlayerAddSourceEx(AvPlayerInternal* h, uint32_t uri_type, const void* source_details) { - (void)h; - (void)uri_type; - (void)source_details; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerChangeStream(AvPlayerInternal* h, uint32_t old_stream_id, uint32_t new_stream_id) { - (void)h; - (void)old_stream_id; - (void)new_stream_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerClose(AvPlayerInternal* h) { - (void)h; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -uint64_t APS5_VABI sceAvPlayerCurrentTime(AvPlayerInternal* h) { - (void)h; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerDisableStream(AvPlayerInternal* h, uint32_t stream_id) { - (void)h; - (void)stream_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerEnableStream(AvPlayerInternal* h, uint32_t stream_id) { - (void)h; - (void)stream_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -Bool APS5_VABI sceAvPlayerGetAudioData(AvPlayerInternal* h, AvPlayerFrameInfo* audio_info) { - (void)h; - (void)audio_info; - NotImplemented_nid_no_patch(__func__); - return {}; -} - -int APS5_VABI sceAvPlayerGetStreamInfo(AvPlayerInternal* h, uint32_t stream_id, void* info) { - (void)h; - (void)stream_id; - (void)info; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerGetStreamInfoEx(AvPlayerInternal* h, uint32_t stream_id, void* info) { - (void)h; - (void)stream_id; - (void)info; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -Bool APS5_VABI sceAvPlayerGetVideoDataEx(AvPlayerInternal* h, AvPlayerFrameInfoEx* video_info) { - (void)h; - (void)video_info; - NotImplemented_nid_no_patch(__func__); - return {}; -} - -AvPlayerInternal* APS5_VABI sceAvPlayerInit(AvPlayerInitData* init) { - (void)init; - NotImplemented_nid_no_patch(__func__); - return nullptr; -} - -int APS5_VABI sceAvPlayerInitEx(const void* init_ex, AvPlayerInternal** handle) { - (void)init_ex; - (void)handle; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -Bool APS5_VABI sceAvPlayerIsActive(AvPlayerInternal* h) { - (void)h; - NotImplemented_nid_no_patch(__func__); - return {}; -} - -int APS5_VABI sceAvPlayerJumpToTime(AvPlayerInternal* h, uint64_t time_ms) { - (void)h; - (void)time_ms; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerPause(AvPlayerInternal* h) { - (void)h; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerPostInit(AvPlayerInternal* h, const void* post_init) { - (void)h; +int APS5_VABI sceAvPlayerPostInit(void* h, const void* post_init) { (void)post_init; - NotImplemented_nid_no_patch(__func__); - return 0; + return Check(h) != nullptr ? 0 : kErrInvalidParams; } - -int APS5_VABI sceAvPlayerResume(AvPlayerInternal* h) { - (void)h; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerSetAvailableBandwidth(AvPlayerInternal* h, uint32_t start_bandwidth, uint32_t minimum_bandwidth, uint32_t maximum_bandwidth) { - (void)h; - (void)start_bandwidth; - (void)minimum_bandwidth; - (void)maximum_bandwidth; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerSetAvSyncMode(AvPlayerInternal* h, uint32_t sync_mode) { - (void)h; - (void)sync_mode; - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAvPlayerSetLogCallback(void* callback, void* user_data) { (void)callback; (void)user_data; - NotImplemented_nid_no_patch(__func__); return 0; } - -int APS5_VABI sceAvPlayerSetLooping(AvPlayerInternal* h, Bool loop) { - (void)h; - (void)loop; - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAvPlayerAddSource(void* h, const char* filename) { + return AddSourceCommon(Check(h), filename); +} +int APS5_VABI sceAvPlayerAddSourceEx(void* h, std::uint32_t uri_type, const void* source_details) { + (void)uri_type; + // SceAvPlayerSourceDetails begins with { const char* name; uint32_t length; }. + const char* name = source_details != nullptr ? *static_cast(source_details) : nullptr; + return AddSourceCommon(Check(h), name); +} +int APS5_VABI sceAvPlayerStart(void* h) { + Player* p = Check(h); + if (p == nullptr) return kErrInvalidParams; + LogLifecycleCall("Start(before)", p); + bool play = false; + { + std::lock_guard g(p->lock); + if (p->haveSource && !p->playing && !p->stopFired) { p->playing = true; p->paused = false; play = true; } + } + if (play) Fire(p, kEventPlay); return 0; } - -int APS5_VABI sceAvPlayerSetTrickSpeed(AvPlayerInternal* h, int32_t trick_speed) { - (void)h; - (void)trick_speed; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerStart(AvPlayerInternal* h) { - (void)h; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerStartEx(AvPlayerInternal* h, const void* start_info_ex) { - (void)h; +int APS5_VABI sceAvPlayerStartEx(void* h, const void* start_info_ex) { (void)start_info_ex; - NotImplemented_nid_no_patch(__func__); + return sceAvPlayerStart(h); +} +int APS5_VABI sceAvPlayerPause(void* h) { + Player* p = Check(h); + if (p == nullptr) return kErrInvalidParams; + LogLifecycleCall("Pause(before)", p); + bool notify = false; + { + std::lock_guard g(p->lock); + if (p->playing && !p->paused) { p->paused = true; HaltClock(*p); notify = true; } + } + if (notify) Fire(p, kEventPause); return 0; } - -int APS5_VABI sceAvPlayerStop(AvPlayerInternal* h) { - (void)h; - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAvPlayerResume(void* h) { + Player* p = Check(h); + if (p == nullptr) return kErrInvalidParams; + LogLifecycleCall("Resume(before)", p); + bool notify = false; + { + std::lock_guard g(p->lock); + if (p->playing && p->paused) { p->paused = false; notify = true; } + } + if (notify) Fire(p, kEventPlay); return 0; } - -int APS5_VABI sceAvPlayerStreamCount(AvPlayerInternal* h) { - (void)h; - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAvPlayerStop(void* h) { + Player* p = Check(h); + if (p == nullptr) return kErrInvalidParams; + LogLifecycleCall("Stop(before)", p); + bool notify = false; + { + std::lock_guard g(p->lock); + if (p->playing && !p->stopFired) { p->playing = false; HaltClock(*p); p->stopFired = true; notify = true; } + p->haveSource = false; + } + if (notify) Fire(p, kEventStop); + return 0; +} +int APS5_VABI sceAvPlayerClose(void* h) { + Player* p = Check(h); + if (p == nullptr) return kErrInvalidParams; + std::vector frames; + { + std::lock_guard g(p->lock); + p->magic = 0; + frames.swap(p->frames); + } + // The frame storage came from the title's allocator, so it goes back to it: a title that opens a player per + // movie would otherwise lose every buffer of the clip on each close. + if (p->deallocTexture != nullptr) + for (auto* frame : frames) p->deallocTexture(p->memObject, frame); + delete p; + return 0; +} +// bool: true while the player is presenting. See EndStream: the clip is finite, so once it has run out the poll +// that finds no frame left reports the end (STOP event) and IsActive turns false. Until then it stays true, which +// is what keeps a title that pumps the getters rather than IsActive from being told the movie ended early. +Bool APS5_VABI sceAvPlayerIsActive(void* h) { + Player* p = Check(h); + if (p == nullptr) return 0; + if (EndStream(p)) Fire(p, kEventStop); + std::lock_guard g(p->lock); + return p->playing ? 1 : 0; +} +// The clip's own media clock: it runs on wall time while the player presents, so it agrees with the timestamps +// on the frames handed out and with the duration the stream info reports. +std::uint64_t APS5_VABI sceAvPlayerCurrentTime(void* h) { + Player* p = Check(h); + if (p == nullptr) return 0; + std::lock_guard g(p->lock); + const auto time_ms = MediaTime(*p); + return time_ms > kStreamDurationMs ? kStreamDurationMs : time_ms; +} +// A seek within a clip whose frames are all identical is a clock move, and that is all the title can be +// waiting for: a pull loop that discards any frame no newer than the seek target would otherwise throw away +// every frame from here on. +int APS5_VABI sceAvPlayerJumpToTime(void* h, std::uint64_t time_ms) { + Player* p = Check(h); + if (p == nullptr) return kErrInvalidParams; + LogLifecycleCall("JumpToTime(before)", p); + std::fprintf(stderr, "[AVPLIFE] JumpToTime time_ms=%llu\n", static_cast(time_ms)); + std::fflush(stderr); + { + std::lock_guard g(p->lock); + p->bankedMs = time_ms > kStreamDurationMs ? kStreamDurationMs : time_ms; + p->clockAnchor = {}; + } + std::uint32_t warning = kWarningJumpComplete; + Fire(p, kEventWarning, &warning); + return 0; +} +int APS5_VABI sceAvPlayerSetLooping(void* h, Bool loop) { + Player* p = Check(h); + if (p == nullptr) return kErrInvalidParams; + p->looping = loop != 0; + return 0; +} +int APS5_VABI sceAvPlayerSetAvSyncMode(void* h, std::uint32_t sync_mode) { + (void)sync_mode; + return Check(h) != nullptr ? 0 : kErrInvalidParams; +} +int APS5_VABI sceAvPlayerSetTrickSpeed(void* h, std::int32_t trick_speed) { + (void)trick_speed; + return Check(h) != nullptr ? 0 : kErrInvalidParams; +} +int APS5_VABI sceAvPlayerSetAvailableBandwidth(void* h, std::uint32_t start_bandwidth, std::uint32_t minimum_bandwidth, std::uint32_t maximum_bandwidth) { + (void)start_bandwidth; + (void)minimum_bandwidth; + (void)maximum_bandwidth; + return Check(h) != nullptr ? 0 : kErrInvalidParams; +} +// One (placeholder) video stream once a source is present; the stream table below describes it as an empty 1920x1080 clip. +int APS5_VABI sceAvPlayerStreamCount(void* h) { + Player* p = Check(h); + if (p == nullptr) return kErrInvalidParams; + std::lock_guard g(p->lock); + return p->haveSource ? 1 : 0; +} +int APS5_VABI sceAvPlayerEnableStream(void* h, std::uint32_t stream_id) { + (void)stream_id; + return Check(h) != nullptr ? 0 : kErrInvalidParams; +} +int APS5_VABI sceAvPlayerDisableStream(void* h, std::uint32_t stream_id) { + (void)stream_id; + return Check(h) != nullptr ? 0 : kErrInvalidParams; +} +int APS5_VABI sceAvPlayerChangeStream(void* h, std::uint32_t old_stream_id, std::uint32_t new_stream_id) { + (void)old_stream_id; + (void)new_stream_id; + return Check(h) != nullptr ? 0 : kErrInvalidParams; +} +// SceAvPlayerStreamInfo (32 bytes): u32 type (1 video, 2 audio), u8 reserved[4], details[16], u64 duration (ms). +// Video details: u32 width, u32 height, float aspect, char language[4]. +int APS5_VABI sceAvPlayerGetStreamInfo(void* h, std::uint32_t stream_id, void* info) { + Player* p = Check(h); + if (p == nullptr || info == nullptr) return kErrInvalidParams; + std::lock_guard g(p->lock); + if (!p->haveSource || stream_id != 0) return kErrOperationFailed; + std::uint8_t* out = static_cast(info); + std::memset(out, 0, 32); + const std::uint32_t type = 1; + const std::uint32_t width = kFrameWidth; + const std::uint32_t height = kFrameHeight; + const float aspect = static_cast(kFrameWidth) / static_cast(kFrameHeight); + const std::uint64_t duration = kStreamDurationMs; + std::memcpy(out + 0, &type, 4); + std::memcpy(out + 8, &width, 4); + std::memcpy(out + 12, &height, 4); + std::memcpy(out + 16, &aspect, 4); + std::memcpy(out + 24, &duration, 8); + return 0; +} +// SceAvPlayerStreamInfoEx (104 bytes): u64 size, u32 type, reserved[4], details[80], u64 duration. The details +// union holds the same wide video description the frame getters publish, so a title that sizes its movie +// surfaces from the stream info instead of from a frame gets one answer, not two. +int APS5_VABI sceAvPlayerGetStreamInfoEx(void* h, std::uint32_t stream_id, void* info) { + Player* p = Check(h); + if (p == nullptr || info == nullptr) return kErrInvalidParams; + std::lock_guard g(p->lock); + if (!p->haveSource || stream_id != 0) return kErrOperationFailed; + std::uint8_t* out = static_cast(info); + std::uint64_t size = 0; + std::memcpy(&size, out, 8); + std::memset(out, 0, 104); + std::memcpy(out, &size, 8); + const std::uint32_t type = 1; + const std::uint64_t duration = kStreamDurationMs; + std::memcpy(out + 8, &type, 4); + VideoDetailsEx details; + WriteVideoDetails(details); + std::memcpy(out + 16, &details, sizeof(details)); + std::memcpy(out + 96, &duration, 8); + return 0; +} +// The data getters are the frame source as well as the end-of-stream report: they hand out whichever frame of +// the blank clip the media clock has reached, and the poll that finds the clip spent is the one that reports the +// stream as finished. Each site logs its own first three calls, so a run shows which of these the title pumps. +Bool APS5_VABI sceAvPlayerGetVideoData(void* h, void* video_info) { + Player* p = Check(h); + if (p == nullptr) return 0; + APS5_HIT("AVP", "GetVideoData"); + return PollVideo(p, video_info, false); +} +Bool APS5_VABI sceAvPlayerGetVideoDataEx(void* h, void* video_info) { + Player* p = Check(h); + if (p == nullptr) return 0; + APS5_HIT("AVP", "GetVideoDataEx"); + return PollVideo(p, video_info, true); +} +Bool APS5_VABI sceAvPlayerGetAudioData(void* h, void* audio_info) { + (void)audio_info; + Player* p = Check(h); + if (p == nullptr) return 0; + APS5_HIT("AVP", "GetAudioData"); + if (EndStream(p)) Fire(p, kEventStop); return 0; } - } diff --git a/core/libs/prx/libSceAvPlayer/Export.cpp b/core/libs/prx/libSceAvPlayer/Export.cpp index 25ed90a9d..06e4ac80c 100644 --- a/core/libs/prx/libSceAvPlayer/Export.cpp +++ b/core/libs/prx/libSceAvPlayer/Export.cpp @@ -1,177 +1 @@ -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - -extern "C" { - -int APS5_VABI sceAvPlayerAddSource(AvPlayerInternal* h, const char* filename) { - (void)h; - (void)filename; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerAddSourceEx(AvPlayerInternal* h, uint32_t uri_type, const void* source_details) { - (void)h; - (void)uri_type; - (void)source_details; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerChangeStream(AvPlayerInternal* h, uint32_t old_stream_id, uint32_t new_stream_id) { - (void)h; - (void)old_stream_id; - (void)new_stream_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerClose(AvPlayerInternal* h) { - (void)h; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -uint64_t APS5_VABI sceAvPlayerCurrentTime(AvPlayerInternal* h) { - (void)h; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerDisableStream(AvPlayerInternal* h, uint32_t stream_id) { - (void)h; - (void)stream_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerEnableStream(AvPlayerInternal* h, uint32_t stream_id) { - (void)h; - (void)stream_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -Bool APS5_VABI sceAvPlayerGetAudioData(AvPlayerInternal* h, AvPlayerFrameInfo* audio_info) { - (void)h; - (void)audio_info; - NotImplemented_nid_no_patch(__func__); - return {}; -} - -int APS5_VABI sceAvPlayerGetStreamInfo(AvPlayerInternal* h, uint32_t stream_id, void* info) { - (void)h; - (void)stream_id; - (void)info; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -Bool APS5_VABI sceAvPlayerGetVideoData(AvPlayerInternal* h, AvPlayerFrameInfo* video_info) { - (void)h; - (void)video_info; - NotImplemented_nid_no_patch(__func__); - return {}; -} - -Bool APS5_VABI sceAvPlayerGetVideoDataEx(AvPlayerInternal* h, AvPlayerFrameInfoEx* video_info) { - (void)h; - (void)video_info; - NotImplemented_nid_no_patch(__func__); - return {}; -} - -AvPlayerInternal* APS5_VABI sceAvPlayerInit(AvPlayerInitData* init) { - (void)init; - NotImplemented_nid_no_patch(__func__); - return nullptr; -} - -int APS5_VABI sceAvPlayerInitEx(const void* init_ex, AvPlayerInternal** handle) { - (void)init_ex; - (void)handle; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -Bool APS5_VABI sceAvPlayerIsActive(AvPlayerInternal* h) { - (void)h; - NotImplemented_nid_no_patch(__func__); - return {}; -} - -int APS5_VABI sceAvPlayerJumpToTime(AvPlayerInternal* h, uint64_t time_ms) { - (void)h; - (void)time_ms; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerPause(AvPlayerInternal* h) { - (void)h; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerPostInit(AvPlayerInternal* h, const void* post_init) { - (void)h; - (void)post_init; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerResume(AvPlayerInternal* h) { - (void)h; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerSetAvSyncMode(AvPlayerInternal* h, uint32_t sync_mode) { - (void)h; - (void)sync_mode; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerSetLogCallback(void* callback, void* user_data) { - (void)callback; - (void)user_data; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerSetLooping(AvPlayerInternal* h, Bool loop) { - (void)h; - (void)loop; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerSetTrickSpeed(AvPlayerInternal* h, int32_t trick_speed) { - (void)h; - (void)trick_speed; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerStart(AvPlayerInternal* h) { - (void)h; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerStop(AvPlayerInternal* h) { - (void)h; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerStreamCount(AvPlayerInternal* h) { - (void)h; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -} +#include "prx/libSceAvPlayer.native/Export.cpp" From 9f1c680b38a7d2a3e6b5f928fcf73a73f29caf58 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Mon, 28 Sep 2026 13:35:53 +0300 Subject: [PATCH 18/46] fix(agc): preserve per-draw input buffer contents until gpu completion --- .../Graphics/include/ShaderResources.hpp | 14 ++++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 35 +++++++--- .../Graphics/src/ShaderResources.cpp | 64 +++++++++++++++++++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 38 +++++++++++ 4 files changed, 142 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 67b0511e0..2092a2b68 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -101,6 +101,20 @@ public: ShaderResources& operator=(const ShaderResources&) = delete; VkDescriptorSetLayout Layout() const; void Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const; + struct DrawBindings { + DrawBindings() = default; + DrawBindings(const DrawBindings&) = delete; + DrawBindings& operator=(const DrawBindings&) = delete; + struct Snapshot { + std::uint64_t address; + std::shared_ptr buffer; + }; + DescriptorCache* cache = nullptr; + DescriptorCache::SetAllocation allocation; + std::vector snapshots; + ~DrawBindings(); + }; + std::shared_ptr PrepareDrawBindings(Recorder& recorder) const; void WriteBack(); // Deferred completion: MarkGpuWrites registers the results the recorded work leaves on the GPU // (storage images stay there; buffer ranges are noted so CPU reads wait); WriteBackBuffers runs diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 3fd283069..73e9dfa33 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1005,7 +1005,7 @@ bool CaptureInputsEnabled() { return enabled; } -void captureInputs(const Context& context, Recorder& recorder, VkCommandBuffer commands, const ShaderResources& resources, std::uint64_t target) { +void captureInputs(const Context& context, Recorder& recorder, VkCommandBuffer commands, const ShaderResources& resources, const std::shared_ptr& bindings, std::uint64_t target) { struct Sample { std::uint64_t address; std::size_t offset; @@ -1018,22 +1018,33 @@ void captureInputs(const Context& context, Recorder& recorder, VkCommandBuffer c const auto batch = static_cast(recorder.Submissions() + 1); std::vector samples; std::size_t total = 0; + const auto addSample = [&](std::uint64_t address, std::size_t bytes, VkBuffer source, VkDeviceSize offset, const std::byte* expected) { + if (bytes == 0 || bytes > 512) return; + Require(offset % 4 == 0 && bytes % 4 == 0, "capture input is not aligned for a Vulkan buffer copy"); + Require(total + bytes <= 65536, "capture inputs exceed 64 KiB per draw"); + Sample sample{address, total, std::vector(bytes), source, offset}; + std::memcpy(sample.expected.data(), expected, bytes); + total += bytes; + samples.push_back(std::move(sample)); + }; + if (bindings != nullptr) { + for (const auto& snapshot : bindings->snapshots) { + const auto bytes = snapshot.buffer->Bytes(); + addSample(snapshot.address, bytes.size(), snapshot.buffer->Handle(), 0, bytes.data()); + } + } for (const auto& [begin, end] : resources.InPlaceReads()) { Require(end >= begin, "invalid capture input range"); const auto bytes = static_cast(end - begin); if (bytes == 0 || bytes > 512) continue; + if (bindings != nullptr && std::any_of(bindings->snapshots.begin(), bindings->snapshots.end(), [&](const auto& snapshot) { return snapshot.address < end && begin < snapshot.address + snapshot.buffer->Bytes().size(); })) continue; if (resources.WritesOverlap(begin, bytes) || recorder.PendingWriteOverlaps(begin, bytes)) { CaptureTrace::Log("input-skip draw=%llu batch=%llu address=%llx bytes=%zu reason=gpu-writer", draw, batch, static_cast(begin), bytes); continue; } const auto* imported = HostImportFor(context, begin, bytes); Require(imported != nullptr, "capture input has no host import"); - Require(begin % 4 == 0 && bytes % 4 == 0, "capture input is not aligned for a Vulkan buffer copy"); - Require(total + bytes <= 65536, "capture inputs exceed 64 KiB per draw"); - Sample sample{begin, total, std::vector(bytes), imported->buffer, begin - imported->base}; - std::memcpy(sample.expected.data(), reinterpret_cast(begin), bytes); - total += bytes; - samples.push_back(std::move(sample)); + addSample(begin, bytes, imported->buffer, begin - imported->base, reinterpret_cast(begin)); } CaptureTrace::Log("input-capture draw=%llu batch=%llu target=%llx ranges=%zu bytes=%zu", draw, batch, static_cast(target), samples.size(), total); if (samples.empty()) return; @@ -1097,12 +1108,13 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam if (recorder->HasQueuedKeyStores() && (resources.HoldsLease() || recorder->AnyQueuedKeyStore(touches))) recorder->FlushKeyStores(); // A queued label store over such memory likewise (Recorder::RecordStore). if (recorder->HasQueuedStores() && (resources.HoldsLease() || recorder->AnyQueuedStore(touches))) recorder->FlushStores(); + const auto drawBindings = resources.PrepareDrawBindings(*recorder); const bool capture = CaptureInputsEnabled(); const bool continued = !capture && !readsTarget && !gpuIndirect && recorder->ContinuesRenderPass(passKey); outcome.passContinued = continued; outcome.passBegun = !continued; const auto commands = continued ? recorder->CommandsInRenderPass() : recorder->Commands(); - if (capture) captureInputs(context, *recorder, commands, resources, record.targets.empty() || record.targets.front() == nullptr ? 0 : record.targets.front()->Descriptor().baseAddress); + if (capture) captureInputs(context, *recorder, commands, resources, drawBindings, record.targets.empty() || record.targets.front() == nullptr ? 0 : record.targets.front()->Descriptor().baseAddress); // The draw's [gputime] class range: from its first barrier to the pass's trailing barrier (a // continued draw lies inside its pass's range). const auto drawTiming = !continued ? recorder->BeginGpuTiming(CommandClass::Draw) : Recorder::NoTiming; @@ -1134,7 +1146,12 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam record.pipeline->Begin(commands, *record.framebuffer, state.renderExtent, state.viewport, state.scissor); } APS5_LOG_CHARS_OUT_DEBUG("Pipeline Begin OK"); - resources.Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout()); + if (drawBindings != nullptr) { + const auto set = drawBindings->allocation.set; + context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout(), 0, 1, &set, 0, nullptr); + } else { + resources.Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout()); + } APS5_LOG_CHARS_OUT_DEBUG("Resources bound"); if (record.pushBytes != nullptr) record.pipeline->PushConstants(commands, record.pushStages, *record.pushBytes); else record.pipeline->PushConstants(commands, shaders); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 35c605c26..d84bf94de 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2462,6 +2462,70 @@ VkDescriptorSetLayout ShaderResources::Layout() const { return _layout; } +ShaderResources::DrawBindings::~DrawBindings() { + if (cache != nullptr && allocation.set != VK_NULL_HANDLE) cache->Free(allocation); +} + +std::shared_ptr ShaderResources::PrepareDrawBindings(Recorder& recorder) const { + if (_set == VK_NULL_HANDLE || usesBda) return {}; + const auto reads = guestMemory.InPlaceReads(); + auto result = std::make_shared(); + std::vector selected; + for (std::size_t index = 0; index < allocations.size(); ++index) { + const auto& item = allocations[index]; + if (!item.guest || item.written || guestMemory.WritesOverlap(item.address, item.size)) continue; + const bool direct = std::any_of(reads.begin(), reads.end(), [&](const auto& range) { return item.address >= range.first && item.address < range.second && item.size <= range.second - item.address; }); + if (!direct || recorder.PendingWriteOverlaps(item.address, item.size)) continue; + auto buffer = std::make_shared(context, item.size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::memcpy(buffer->Bytes().data(), reinterpret_cast(item.address), item.size); + selected.push_back(index); + result->snapshots.push_back({item.address, std::move(buffer)}); + CaptureTrace::Log("draw-snapshot batch=%llu address=%llx bytes=%zu", static_cast(recorder.Submissions() + 1), static_cast(item.address), item.size); + } + if (selected.empty()) return {}; + Require(context.descriptorCache != nullptr, "draw snapshots require a descriptor cache"); + std::map counts; + for (const auto& binding : bindings) counts[binding.layout.descriptorType] += binding.layout.descriptorCount; + std::vector sizes; + for (const auto& [type, count] : counts) sizes.push_back({type, count}); + result->cache = context.descriptorCache; + result->allocation = result->cache->Allocate(_layout, sizes); + Require(result->allocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed"); + std::vector copies; + for (const auto& binding : bindings) { + VkCopyDescriptorSet copy{VK_STRUCTURE_TYPE_COPY_DESCRIPTOR_SET}; + copy.srcSet = _set; + copy.srcBinding = binding.layout.binding; + copy.dstSet = result->allocation.set; + copy.dstBinding = binding.layout.binding; + copy.descriptorCount = binding.layout.descriptorCount; + copies.push_back(copy); + } + const auto update = context.Resolved(&DeviceFunctions::updateDescriptorSets, "vkUpdateDescriptorSets"); + update(context.device, 0, nullptr, static_cast(copies.size()), copies.data()); + std::vector infos; + infos.reserve(selected.size()); + for (const auto& snapshot : result->snapshots) infos.push_back({snapshot.buffer->Handle(), 0, snapshot.buffer->Bytes().size()}); + std::vector writes; + for (const auto& binding : bindings) { + for (std::size_t element = 0; element < binding.allocations.size(); ++element) { + const auto found = std::find(selected.begin(), selected.end(), binding.allocations[element]); + if (found == selected.end()) continue; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = result->allocation.set; + write.dstBinding = binding.layout.binding; + write.dstArrayElement = static_cast(element); + write.descriptorCount = 1; + write.descriptorType = binding.layout.descriptorType; + write.pBufferInfo = &infos[static_cast(found - selected.begin())]; + writes.push_back(write); + } + } + update(context.device, static_cast(writes.size()), writes.data(), 0, nullptr); + recorder.Keep(result); + return result; +} + void ShaderResources::Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const { if (_set == VK_NULL_HANDLE) return; context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, bindPoint, layout, 0, 1, &_set, 0, nullptr); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 881487a71..98eb4a645 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -653,6 +653,44 @@ void resourceReadTests(const Device& device, Recorder& recorder) { Require(!recorder.PendingReadOverlaps(element, 16), "the build's read refuses after its batch ran"); recorder.Sync(); } + std::weak_ptr snapshotLifetime; + { + auto snapshotContext = context; + DescriptorCache cache(snapshotContext); + snapshotContext.descriptorCache = &cache; + Recorder snapshotRecorder(snapshotContext); + snapshotRecorder.Activate(); + ShaderResources resources(snapshotContext, compute); + auto first = resources.PrepareDrawBindings(snapshotRecorder); + Require(first != nullptr && first->snapshots.size() == 1, "read-only draw input was not snapshotted"); + std::memset(reinterpret_cast(element), 0x22, elementBytes); + auto second = resources.PrepareDrawBindings(snapshotRecorder); + Require(second != nullptr && second->snapshots.size() == 1, "cached draw input was not snapshotted again"); + Require(first->allocation.set != second->allocation.set, "in-flight draws share mutable descriptor bindings"); + std::memset(reinterpret_cast(element), 0x33, elementBytes); + auto downloaded = std::make_shared(snapshotContext, elementBytes * 2, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const auto commands = snapshotRecorder.Commands(); + RecordMemoryBarrier(snapshotContext, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + const auto copy = snapshotContext.Function("vkCmdCopyBuffer"); + VkBufferCopy region{0, 0, elementBytes}; + copy(commands, first->snapshots[0].buffer->Handle(), downloaded->Handle(), 1, ®ion); + region.dstOffset = elementBytes; + copy(commands, second->snapshots[0].buffer->Handle(), downloaded->Handle(), 1, ®ion); + RecordMemoryBarrier(snapshotContext, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + snapshotLifetime = first; + first.reset(); + second.reset(); + Require(!snapshotLifetime.expired(), "draw snapshot was released before its GPU batch"); + snapshotRecorder.Sync(); + const auto contents = downloaded->Bytes(); + Require(std::all_of(contents.begin(), contents.begin() + elementBytes, [](std::byte value) { return value == std::byte{0x11}; }), "first draw observed overwritten input"); + Require(std::all_of(contents.begin() + elementBytes, contents.end(), [](std::byte value) { return value == std::byte{0x22}; }), "second draw observed overwritten input"); + snapshotRecorder.NotePendingWrite(element, elementBytes); + Require(resources.PrepareDrawBindings(snapshotRecorder) == nullptr, "GPU-produced input was replaced with stale CPU memory"); + snapshotRecorder.Sync(); + } + Require(snapshotLifetime.expired(), "draw snapshots outlived their recorder"); + recorder.Activate(); { GuestAllocations::Mutation mutation; mutation.Remove(block); From 64732a64b6b4da1d37a64944c8c1ba810b5b6654 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Mon, 28 Sep 2026 14:18:35 +0300 Subject: [PATCH 19/46] fix(build): link json and fiber tests with libc --- core/libs/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 97021bfbf..818628c5b 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -387,7 +387,7 @@ if(BUILD_TESTING) endif() add_executable(guest_json_tests tests/GuestJson.cpp) target_include_directories(guest_json_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(guest_json_tests PRIVATE libSceJson2) + target_link_libraries(guest_json_tests PRIVATE libSceJson2 libc) add_test(NAME guest_json COMMAND guest_json_tests) if(WIN32) set_tests_properties(guest_json PROPERTIES ENVIRONMENT_MODIFICATION @@ -395,7 +395,7 @@ if(BUILD_TESTING) endif() add_executable(fiber_tests tests/Fiber.cpp) target_include_directories(fiber_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(fiber_tests PRIVATE libSceFiber) + target_link_libraries(fiber_tests PRIVATE libSceFiber libc) add_test(NAME fiber COMMAND fiber_tests) if(WIN32) set_tests_properties(fiber PROPERTIES ENVIRONMENT_MODIFICATION From 8e78ea0c28d2390d8447d97283afd654fa5a0c8f Mon Sep 17 00:00:00 2001 From: boykopovar Date: Mon, 28 Sep 2026 16:28:11 +0300 Subject: [PATCH 20/46] fix(nid): always static linking for nid patcher --- core/libs/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 818628c5b..aa3c058ff 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -17,6 +17,7 @@ target_include_directories(nid_patcher PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/i target_compile_features(nid_patcher PRIVATE cxx_std_20) if(MINGW) + target_link_options(nid_patcher PRIVATE -static -static-libgcc -static-libstdc++) add_compile_options(-fno-asynchronous-unwind-tables) endif() From b4c18877524bcf8aed27f3cd95b20c552c53cf7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 28 Sep 2026 15:42:29 +0200 Subject: [PATCH 21/46] docs: add pull request template with the review checklist Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/pull_request_template.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..7450248bc --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ +### What + +### Tested + + +### Checklist +- [ ] Based on current `main`; no other open PR implements the same functions +- [ ] One topic per PR; follow-ups go in a new PR +- [ ] No comments in code except [technical debt](https://github.com/boykopovar/AnyPS5/blob/main/docs/CONVENTIONS.md) +- [ ] Unimplemented paths throw (`NotImplemented_nid_no_patch`); silent stubs are listed in [TechnicalDebt](https://github.com/boykopovar/AnyPS5/blob/main/docs/TechnicalDebt.md#silent-stubs) +- [ ] No notes, investigation `.md` files or images added to the repository (attach them to this PR) +- [ ] New third-party code is a submodule built from source +- [ ] Generic behaviour, not specific to one title +- [ ] Depends on: +- [ ] AI-assisted: From a8e48f48aaecfcc3eebe8019da945d1b90798f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 28 Sep 2026 15:47:51 +0200 Subject: [PATCH 22/46] ci: label pull requests by area Co-Authored-By: Claude --- .github/labeler.yml | 45 +++++++++++++++++++++++++++++++ .github/workflows/area-labels.yml | 29 ++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/area-labels.yml diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000..44971015c --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,45 @@ +relinker: + - changed-files: + - any-glob-to-any-file: 'core/relinker/**' + +shader: + - changed-files: + - any-glob-to-any-file: 'core/shader/**' + +agc: + - changed-files: + - any-glob-to-any-file: 'core/libs/prx/libSceAgc*/**' + +libc: + - changed-files: + - any-glob-to-any-file: 'core/libs/prx/libc/**' + +libkernel: + - changed-files: + - any-glob-to-any-file: 'core/libs/prx/libkernel/**' + +prx: + - changed-files: + - all-globs-to-any-file: + - 'core/libs/prx/**' + - '!core/libs/prx/libSceAgc*/**' + - '!core/libs/prx/libc/**' + - '!core/libs/prx/libkernel/**' + +build: + - changed-files: + - any-glob-to-any-file: + - '**/CMakeLists.txt' + - '**/*.cmake' + - '.gitmodules' + - '3rdparty/**' + +docs: + - changed-files: + - any-glob-to-any-file: + - '**/*.md' + - 'docs/**' + +ci: + - changed-files: + - any-glob-to-any-file: '.github/**' diff --git a/.github/workflows/area-labels.yml b/.github/workflows/area-labels.yml new file mode 100644 index 000000000..fb0dbe9b8 --- /dev/null +++ b/.github/workflows/area-labels.yml @@ -0,0 +1,29 @@ +name: Area labels + +on: + schedule: + - cron: '30 * * * *' + workflow_dispatch: + +permissions: {} + +jobs: + label: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - id: prs + env: + GH_TOKEN: ${{ github.token }} + run: | + { + echo 'numbers<> "$GITHUB_OUTPUT" + - if: steps.prs.outputs.numbers != '' + uses: actions/labeler@v7 + with: + pr-number: ${{ steps.prs.outputs.numbers }} From d8db8192b49750323c37c34e3b8730410a15e21e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 28 Sep 2026 15:47:51 +0200 Subject: [PATCH 23/46] ci: label pull requests that conflict with main Co-Authored-By: Claude --- .github/workflows/conflict-label.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/conflict-label.yml diff --git a/.github/workflows/conflict-label.yml b/.github/workflows/conflict-label.yml new file mode 100644 index 000000000..d6183c9eb --- /dev/null +++ b/.github/workflows/conflict-label.yml @@ -0,0 +1,21 @@ +name: Conflict label + +on: + push: + branches: [main] + schedule: + - cron: '0 * * * *' + workflow_dispatch: + +permissions: {} + +jobs: + label: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: eps1lon/actions-label-merge-conflict@v3 + with: + dirtyLabel: needs rebase + repoToken: ${{ secrets.GITHUB_TOKEN }} From d074f6e36f754ebb4278817093860848c3a34522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 28 Sep 2026 16:06:33 +0200 Subject: [PATCH 24/46] docs: link the pull request template to the contributing guide Co-Authored-By: Claude --- .github/pull_request_template.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7450248bc..f895a33e9 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,6 +4,7 @@ ### Checklist + - [ ] Based on current `main`; no other open PR implements the same functions - [ ] One topic per PR; follow-ups go in a new PR - [ ] No comments in code except [technical debt](https://github.com/boykopovar/AnyPS5/blob/main/docs/CONVENTIONS.md) From 5ad7c5793a0bb070219621336a8dbdff53bf54f5 Mon Sep 17 00:00:00 2001 From: adriadam10 Date: Sun, 27 Sep 2026 19:44:15 +0200 Subject: [PATCH 25/46] ci: build and test on Linux and Windows Builds with tests enabled and runs ctest on every push to main and every pull request: GCC on Linux, and on Windows the MinGW-w64 winlibs GCC 15.2.0 toolchain required by the README. ccache and the toolchain are cached, and only pushes to main save the ccache, so pull requests reuse it without evicting it. Windows build steps run in cmd because Git Bash puts its own mingw64 DLLs first in PATH, which breaks build tools such as nid_patcher that link libstdc++ dynamically. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/build.yml | 70 +++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..feef942e8 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,70 @@ +name: Build + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: build-${{ github.ref }} + cancel-in-progress: true + +env: + CONFIGURE: >- + -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON + -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ + -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + +jobs: + linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + submodules: true + - uses: hendrikmuhs/ccache-action@v1.2.24 + with: + key: linux + save: ${{ github.event_name == 'push' }} + evict-old-files: job + - run: cmake -S . -B build $CONFIGURE + - run: cmake --build build + - run: ctest --test-dir build --output-on-failure + + windows: + runs-on: windows-latest + env: + WINLIBS_RELEASE: 15.2.0posix-14.0.0-ucrt-r7 + WINLIBS_ZIP: winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v7 + with: + submodules: true + - id: toolchain + uses: actions/cache@v6 + with: + path: C:\winlibs + key: ${{ env.WINLIBS_ZIP }} + - if: steps.toolchain.outputs.cache-hit != 'true' + run: | + curl -sSfL -o "$RUNNER_TEMP/winlibs.zip" "https://github.com/brechtsanders/winlibs_mingw/releases/download/$WINLIBS_RELEASE/$WINLIBS_ZIP" + 7z x "$RUNNER_TEMP/winlibs.zip" -oC:/winlibs > /dev/null + - run: echo 'C:\winlibs\mingw64\bin' >> "$GITHUB_PATH" + - uses: hendrikmuhs/ccache-action@v1.2.24 + with: + key: windows + save: ${{ github.event_name == 'push' }} + evict-old-files: job + - shell: cmd + run: cmake -S . -B build %CONFIGURE% + - shell: cmd + run: cmake --build build + - shell: cmd + run: ctest --test-dir build --output-on-failure From 682fb3d73c20d61787336c20fef96f70ff4c8626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 28 Sep 2026 16:29:45 +0200 Subject: [PATCH 26/46] ci: time out hung tests --- .github/workflows/build.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index feef942e8..5b07dfadd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,6 +22,7 @@ env: jobs: linux: runs-on: ubuntu-latest + timeout-minutes: 30 steps: - uses: actions/checkout@v7 with: @@ -33,10 +34,11 @@ jobs: evict-old-files: job - run: cmake -S . -B build $CONFIGURE - run: cmake --build build - - run: ctest --test-dir build --output-on-failure + - run: ctest --test-dir build --output-on-failure --timeout 120 windows: runs-on: windows-latest + timeout-minutes: 45 env: WINLIBS_RELEASE: 15.2.0posix-14.0.0-ucrt-r7 WINLIBS_ZIP: winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip @@ -67,4 +69,4 @@ jobs: - shell: cmd run: cmake --build build - shell: cmd - run: ctest --test-dir build --output-on-failure + run: ctest --test-dir build --output-on-failure --timeout 120 From 143aec54c21cebb6ad62c2a79a66ed54660acddb Mon Sep 17 00:00:00 2001 From: boykopovar Date: Mon, 28 Sep 2026 18:08:09 +0300 Subject: [PATCH 27/46] fix(agc): preserve inputs for deferred buffer copies --- .../Graphics/src/GuestBufferMemory.cpp | 51 ++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index a14325c52..7e331a10d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libc/include/CpuTopology.hpp" #ifdef _WIN32 #include @@ -1767,6 +1768,8 @@ void GuestBufferMemory::copyRegion(Region& region, bool addressable) { void GuestBufferMemory::recordGpuCopies(std::span copies, bool addressable) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + static const bool capture = std::getenv("APS5_CAPTURE_GPU_COPIES") != nullptr; + Require(!capture || CaptureTrace::Enabled(), "GPU copy capture requires APS5_CAPTURE_TRACE"); auto* recorder = Recorder::Active(); Require(recorder != nullptr, "GPU buffer copies need an active recorder"); // A queued DCC key store or label store over a copied range lands before the copy reads it. @@ -1781,9 +1784,6 @@ void GuestBufferMemory::recordGpuCopies(std::span copies, bool ad for (const auto* region : copies) reads.emplace_back(region->begin, region->end); recorder->NoteAccess(Recorder::CommandClass::StagingIn, Recorder::Access{reads, {}, {}, VK_PIPELINE_STAGE_TRANSFER_BIT}); } - // Every earlier recorded store into the imports (dispatches in place, fills, GPU labels, - // GPU-direct write-backs and the flushes just recorded) and the host's own writes precede the - // copies. A CPU write made after the batch is submitted is the game's race, as on hardware. RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); Recorder::CountBarriers(Recorder::CommandClass::StagingIn); bool stagedAny = false; @@ -1803,10 +1803,47 @@ void GuestBufferMemory::recordGpuCopies(std::span copies, bool ad } if (profile) allocateUs.fetch_add(microsecondsSince(allocateStart), std::memory_order_relaxed); } - CopyBuffer(context, commands, region->copySource, region->begin - region->copySourceBase, region->buffer->Handle(), 0, bytes); - // The copy reads the import in place when the batch runs: a CPU store into the range before - // then would be copied instead of the bytes recorded here. - recorder->NotePendingRead(region->begin, static_cast(bytes), Recorder::ReadKind::GpuCopy); + std::vector expected; + const auto address = region->begin; + const auto batch = static_cast(recorder->Submissions() + 1); + const bool pendingWriter = recorder->PendingWriteOverlaps(address, static_cast(bytes)); + const bool writable = WritesOverlap(address, static_cast(bytes)); + std::shared_ptr inputSnapshot; + auto copySource = region->copySource; + auto copyOffset = region->begin - region->copySourceBase; + if (!pendingWriter && !writable) { + inputSnapshot = std::make_shared(context, static_cast(bytes), VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::memcpy(inputSnapshot->Bytes().data(), reinterpret_cast(address), static_cast(bytes)); + copySource = inputSnapshot->Handle(); + copyOffset = 0; + recorder->Keep(inputSnapshot); + CaptureTrace::Log("copy-input-snapshot batch=%llu address=%llx bytes=%llu", batch, static_cast(address), static_cast(bytes)); + } + if (capture && bytes == 32) { + CaptureTrace::Log("copy-input-candidate batch=%llu address=%llx bytes=%llu pendingWriter=%d writable=%d", batch, static_cast(address), static_cast(bytes), pendingWriter, writable); + if (inputSnapshot != nullptr) { + const auto snapshotBytes = inputSnapshot->Bytes(); + expected.assign(snapshotBytes.begin(), snapshotBytes.end()); + } + } + CopyBuffer(context, commands, copySource, copyOffset, region->buffer->Handle(), 0, bytes); + if (!expected.empty()) { + auto readback = std::make_shared(context, expected.size(), VK_BUFFER_USAGE_TRANSFER_DST_BIT); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(context, commands, region->buffer->Handle(), 0, readback->Handle(), 0, bytes); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_HOST_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + recorder->OnComplete([readback, expected = std::move(expected), address, batch] { + const auto actual = readback->Bytes(); + std::size_t changed = 0; + for (std::size_t index = 0; index < expected.size(); ++index) { + if (expected[index] == actual[index]) continue; + ++changed; + CaptureTrace::Log("copy-input-byte batch=%llu address=%llx offset=%zu cpu=%02x gpu=%02x", batch, static_cast(address), index, std::to_integer(expected[index]), std::to_integer(actual[index])); + } + CaptureTrace::Log("copy-input-result batch=%llu address=%llx bytes=%zu changed=%zu", batch, static_cast(address), expected.size(), changed); + }); + } + if (inputSnapshot == nullptr) recorder->NotePendingRead(region->begin, static_cast(bytes), Recorder::ReadKind::GpuCopy); // Kept by the batch at record time, as every other recorded target is: the caller keeps // the resources only after descriptor and pipeline work that may throw, and a released // buffer would go back to the pool (reused or destroyed) under this copy command. The From 39fd90b050ee3db780febf89f171619f9370d2ac Mon Sep 17 00:00:00 2001 From: boykopovar Date: Mon, 28 Sep 2026 18:30:34 +0300 Subject: [PATCH 28/46] fix(kernel): orderly shutdown when closing the display window --- .../Execution/include/Driver.hpp | 1 + .../Execution/include/WorkerSampler.hpp | 1 + .../libSceAgcDriver/Execution/src/Driver.cpp | 72 ++++++++++++++----- .../Execution/src/WorkerSampler.cpp | 38 +++++++--- .../Graphics/include/GuestBufferMemory.hpp | 2 + .../Graphics/src/GuestBufferMemory.cpp | 46 ++++++------ .../libSceVideoOut/include/VideoOutDriver.hpp | 3 + core/libs/prx/libSceVideoOut/src/Buffer.cpp | 16 +++-- core/libs/prx/libSceVideoOut/src/Event.cpp | 28 ++++++-- core/libs/prx/libSceVideoOut/src/Output.cpp | 63 +++++++++++----- .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 61 ++++++++-------- core/libs/prx/libc/include/Shutdown.hpp | 8 +++ core/libs/prx/libc/src/Process.cpp | 31 ++++++++ .../src/specifics/windows/CrashReport.cpp | 4 +- 14 files changed, 265 insertions(+), 109 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp index d2fde19a7..665b5d159 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp @@ -11,6 +11,7 @@ void Submit(const Packet* packet, std::uint32_t queue); } extern "C" void AgcDriverWaitIdle_nid_postfix(); +extern "C" void AgcDriverShutdown_nid_postfix(); extern "C" void AgcDriverSuspendPoint_nid_postfix(); extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp index 65b272671..ec282a171 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp @@ -5,6 +5,7 @@ namespace AgcDriver { // Starts sampling the calling thread when APS5_SAMPLE_WORKER names an output file. void StartWorkerSampler(); +void StopWorkerSampler(); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp index e02a1ac14..fb2728197 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp @@ -11,6 +11,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libc/include/CpuTopology.hpp" #include "ControlFlow/RequestSerializer.hpp" @@ -460,24 +461,34 @@ public: void Shutdown() { stop(); - CheckFailure(); + std::lock_guard lock(mutex); + rethrowFailure(); } private: void stop() { require(!OnWorkerThread(), "worker cannot stop itself"); std::lock_guard shutdownLock(shutdownMutex); + if (stopped) return; + LibcRequestShutdown_nid_postfix(); { std::lock_guard lock(mutex); stopping = true; + for (auto& [queue, worker] : workers) { + worker.pending.clear(); + worker.queued.store(0, std::memory_order_release); + } } changed.notify_all(); - // No worker is added once `stopping` is set, so the map is stable while the threads finish. for (auto& [queue, worker] : workers) { if (worker.thread.joinable()) worker.thread.join(); } + StopWorkerSampler(); std::lock_guard gpuLock(GuestMemory::GpuMutex()); device.reset(); + replacedDevices.clear(); + Graphics::ShutdownGuestBufferWorkers(); + stopped = true; } public: @@ -506,7 +517,7 @@ public: { std::lock_guard lock(mutex); rethrowFailure(); - require(!stopping, "submission during shutdown"); + checkStopping(); require(accepted != std::numeric_limits::max(), "submission serial overflow"); for (std::size_t cursor = 0; cursor < submission.commands.size();) { const auto* words = submission.commands.data() + cursor; @@ -550,16 +561,17 @@ public: std::unique_lock lock(mutex); const auto target = accepted; ++idleWaiters; - changed.wait(lock, [&] { return failure != nullptr || completed >= target; }); + changed.wait(lock, [&] { return failure != nullptr || stopping || completed >= target; }); --idleWaiters; rethrowFailure(); + checkStopping(); } void SuspendPoint() { require(!OnWorkerThread(), "worker cannot suspend itself"); std::unique_lock lock(mutex); rethrowFailure(); - require(!stopping, "suspend during shutdown"); + checkStopping(); require(accepted != std::numeric_limits::max(), "submission serial overflow"); Submission boundary{}; boundary.serial = accepted + 1; @@ -578,7 +590,7 @@ public: require(output != nullptr, "null video output"); std::lock_guard lock(mutex); rethrowFailure(); - require(!stopping, "video output registration during shutdown"); + checkStopping(); require(outputs.emplace(handle, output).second, "video output already registered"); } @@ -592,9 +604,11 @@ public: // Called per packet and per poll of a wait: the flag keeps the driver mutex out of those loops // until a failure exists, and `failure` itself is still read under the mutex. void CheckFailure() { - if (!failed.load(std::memory_order_acquire)) return; - std::lock_guard lock(mutex); - rethrowFailure(); + if (failed.load(std::memory_order_acquire)) { + std::lock_guard lock(mutex); + rethrowFailure(); + } + checkStopping(); } void ReportFailure(std::exception_ptr error) { @@ -654,7 +668,6 @@ public: if (device == nullptr || device->Window() == nullptr) { if (device) { device->PrepareForReplacement(); - static std::vector> replacedDevices; replacedDevices.push_back(device); } device = std::make_shared(&window); @@ -722,6 +735,8 @@ public: timing.Mark("release_and_callback"); if (profile) reportPresents(waitedMs, inFlight); CheckFailure(); + } catch (const ProcessShutdown&) { + throw; } catch (...) { ReportFailure(std::current_exception()); throw; @@ -1450,13 +1465,16 @@ private: std::map queues; std::map> outputs; DevicePointer device; + std::vector> replacedDevices; + std::stop_token shutdownToken = LibcShutdownToken_nid_postfix(); std::uint64_t accepted = 0; std::uint64_t completed = 0; std::set completedOutOfOrder; std::exception_ptr failure; // Set once `failure` is, so hot loops can check for one without the mutex. std::atomic failed{false}; - bool stopping = false; + std::atomic stopping{false}; + bool stopped = false; bool resetGraphics = false; // Threads in WaitIdle (under `mutex`): a worker's completion notifies only while one waits. std::uint32_t idleWaiters = 0; @@ -1491,6 +1509,10 @@ private: } } + void checkStopping() const { + if (stopping.load(std::memory_order_acquire) || shutdownToken.stop_requested()) throw ProcessShutdown{}; + } + // Summarizes a rejected submission (packet names with counts and the first rejection reason per name). static void dumpPackets(std::span commands, const std::uint32_t* guest = nullptr) { std::map> summary; @@ -6116,7 +6138,7 @@ private: lock.lock(); } rethrowFailure(); - if (pending.empty()) { + if (stopping || shutdownToken.stop_requested() || pending.empty()) { break; } submission = std::move(pending.front()); @@ -6140,6 +6162,8 @@ private: else ++costs.notifiesSkipped; if (profile) costs.completeNs += static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - completeStart).count()); } + } catch (const ProcessShutdown&) { + submission = Submission{}; } catch (...) { const auto error = std::current_exception(); try { @@ -6157,14 +6181,20 @@ private: } -void Submit(const Packet* packet, std::uint32_t queue) { +void Submit(const Packet* packet, std::uint32_t queue) try { Driver::Get().Submit(packet, queue); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } void WaitIdle() { Driver::Get().WaitIdle(); } +void Shutdown() { + Driver::Get().Shutdown(); +} + void RegisterShader(const Shader* shader) { Driver::Get().RegisterShader(shader); } @@ -6199,16 +6229,26 @@ void ReportFailure(std::exception_ptr error) { } -extern "C" void AgcDriverWaitIdle_nid_postfix() { +extern "C" void AgcDriverWaitIdle_nid_postfix() try { AgcDriver::WaitIdle(); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader) { +extern "C" void AgcDriverShutdown_nid_postfix() { + AgcDriver::Shutdown(); +} + +extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader) try { AgcDriver::RegisterShader(shader); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -extern "C" void AgcDriverSuspendPoint_nid_postfix() { +extern "C" void AgcDriverSuspendPoint_nid_postfix() try { AgcDriver::SuspendPoint(); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } extern "C" void AgcDriverRegisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr& output) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp index 7e4d7a381..e48bdbbbc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp @@ -9,8 +9,11 @@ #include #include #include +#include #include #include +#include +#include #include #include #endif @@ -20,6 +23,9 @@ namespace AgcDriver { #ifdef _WIN32 namespace { +std::jthread processSamplerThread; +std::jthread workerSamplerThread; + // Debug aid: APS5_SAMPLE_WORKER= samples the calling thread every millisecond and, every 20 s, // writes "module+offset self inclusive" lines to for offline symbolization with nm. struct Sampler { @@ -29,6 +35,10 @@ struct Sampler { std::map> counts; std::uint64_t samples = 0; + ~Sampler() noexcept(false) { + if (target != nullptr && !CloseHandle(target)) throw std::system_error(GetLastError(), std::system_category(), "Closing sampler thread handle"); + } + void sample() { CONTEXT context{}; context.ContextFlags = CONTEXT_FULL; @@ -137,12 +147,12 @@ struct ProcessSampler { void StartProcessSampler() { const char* path = std::getenv("APS5_SAMPLE_THREADS"); if (path == nullptr) return; - std::thread([path = std::string(path)] { - auto* sampler = new ProcessSampler(); + processSamplerThread = std::jthread([path = std::string(path)](std::stop_token token) { + auto sampler = std::make_unique(); sampler->path = path; sampler->self = GetCurrentThreadId(); auto flushed = std::chrono::steady_clock::now(); - for (;;) { + while (!token.stop_requested()) { std::this_thread::sleep_for(std::chrono::milliseconds(2)); sampler->sample(); if (std::chrono::steady_clock::now() - flushed > std::chrono::seconds(20)) { @@ -150,7 +160,8 @@ void StartProcessSampler() { flushed = std::chrono::steady_clock::now(); } } - }).detach(); + sampler->write(); + }); } } @@ -159,12 +170,12 @@ void StartWorkerSampler() { StartProcessSampler(); const char* path = std::getenv("APS5_SAMPLE_WORKER"); if (path == nullptr) return; - auto* sampler = new Sampler(); + auto sampler = std::make_unique(); sampler->path = path; - DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &sampler->target, THREAD_ALL_ACCESS, FALSE, 0); - std::thread([sampler] { + if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &sampler->target, THREAD_ALL_ACCESS, FALSE, 0)) throw std::system_error(GetLastError(), std::system_category(), "Duplicating sampler thread handle"); + workerSamplerThread = std::jthread([sampler = std::move(sampler)](std::stop_token token) { auto flushed = std::chrono::steady_clock::now(); - for (;;) { + while (!token.stop_requested()) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); sampler->sample(); if (std::chrono::steady_clock::now() - flushed > std::chrono::seconds(20)) { @@ -172,10 +183,19 @@ void StartWorkerSampler() { flushed = std::chrono::steady_clock::now(); } } - }).detach(); + sampler->write(); + }); +} + +void StopWorkerSampler() { + processSamplerThread.request_stop(); + workerSamplerThread.request_stop(); + if (processSamplerThread.joinable()) processSamplerThread.join(); + if (workerSamplerThread.joinable()) workerSamplerThread.join(); } #else void StartWorkerSampler() {} +void StopWorkerSampler() {} #endif } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index bfe9cc0e0..aca08e13e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -11,6 +11,8 @@ namespace AgcDriver::Graphics { +void ShutdownGuestBufferWorkers(); + struct GuestMemorySnapshot { std::uint64_t address; std::span bytes; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 7e331a10d..7213c9085 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -382,19 +383,15 @@ bool compareBlock(const RefreshBlock& block) { return true; } -// Helper threads for the block compares. An address-based build refreshes every writable image range -// (~15 MiB in ~300 blocks) under the device lock, and that memcmp was most of its cost; the blocks -// are handed out by an atomic cursor to the helpers and the calling thread alike, and Run returns -// once every block was taken and finished. The threads are started on first use and never joined -// (they only wait on the condition variable). APS5_MIRROR_REFRESH_THREADS sets the helper count -// (default 3); APS5_NO_PARALLEL_MIRROR_REFRESH=1 compares on the calling thread only. class RefreshPool { public: static RefreshPool& Get() { - // Never destroyed (as WorkerSampler's): the helpers block on its condition variable, which a - // static destructor at std::exit would tear down under them. - static RefreshPool* pool = new RefreshPool; - return *pool; + if (instance == nullptr) instance = new RefreshPool; + return *instance; + } + + static void Shutdown() { + delete std::exchange(instance, nullptr); } // Compares `blocks` and returns how many were copied. @@ -429,19 +426,12 @@ private: const char* text = std::getenv("APS5_MIRROR_REFRESH_THREADS"); const auto requested = text != nullptr ? std::atoi(text) : 3; const auto wanted = disabled ? 0u : static_cast(std::clamp(requested, 0, 16)); - // Only started threads are counted: Run waits for `helpers` finishes, and a thread that could - // not be started would leave it waiting forever. for (unsigned i = 0; i < wanted; ++i) { - try { - std::thread([this] { - CpuTopology::PinHelperThread("mirror refresh"); - helper(); - }).detach(); - ++helpers; - } catch (const std::system_error& error) { - std::fprintf(stderr, "[gpu] mirror refresh helper %u not started: %s\n", i, error.what()); - break; - } + threads.emplace_back([this](std::stop_token token) { + CpuTopology::PinHelperThread("mirror refresh"); + helper(token); + }); + ++helpers; } } @@ -452,13 +442,13 @@ private: copiedBlocks.fetch_add(copied, std::memory_order_relaxed); } - void helper() { + void helper(std::stop_token token) { std::uint64_t seen = 0; for (;;) { std::span blocks; { std::unique_lock lock(mutex); - wake.wait(lock, [&] { return serial != seen; }); + if (!wake.wait(lock, token, [&] { return serial != seen; })) return; seen = serial; blocks = job; } @@ -471,16 +461,18 @@ private: } } + inline static RefreshPool* instance = nullptr; unsigned helpers = 0; std::mutex runMutex; std::mutex mutex; - std::condition_variable wake; + std::condition_variable_any wake; std::condition_variable done; std::span job; std::uint64_t serial = 0; unsigned finished = 0; std::atomic next{0}; std::atomic copiedBlocks{0}; + std::vector threads; }; // The device-lock work before [address, address + bytes) of a writable mirror can be compared with @@ -1357,6 +1349,10 @@ void reportStaging() { } +void ShutdownGuestBufferWorkers() { + RefreshPool::Shutdown(); +} + GuestBufferMemory::~GuestBufferMemory() { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; if (!profile) return; diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index 05a53f833..a24efd186 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -17,6 +17,7 @@ #include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" #include "prx/libSceVideoOut/include/DisplayWindow.hpp" #include "prx/libSceVideoOut/include/BufferReuseTracker.hpp" +#include "prx/libc/include/Shutdown.hpp" #include "SDL.h" #include "SceTypes.hpp" @@ -100,6 +101,7 @@ struct VideoOutConfig { bool opened = false; bool closing = false; std::exception_ptr failure; + std::stop_token shutdownToken = LibcShutdownToken_nid_postfix(); int flipRate = 0; uint64_t lastFlipVblank = 0; std::chrono::steady_clock::time_point lastTimingFlip{}; @@ -117,6 +119,7 @@ struct VideoOutConfig { void Check() const { if (failure) std::rethrow_exception(failure); + if (shutdownToken.stop_requested()) throw ProcessShutdown{}; if (!opened || closing) throw std::runtime_error("VideoOut: port is closed"); } }; diff --git a/core/libs/prx/libSceVideoOut/src/Buffer.cpp b/core/libs/prx/libSceVideoOut/src/Buffer.cpp index 28430fe29..aa40461e1 100644 --- a/core/libs/prx/libSceVideoOut/src/Buffer.cpp +++ b/core/libs/prx/libSceVideoOut/src/Buffer.cpp @@ -35,7 +35,7 @@ void APS5_VABI sceVideoOutSetBufferAttribute2(VideoOutBufferAttribute2* attribut attribute->dcc_control = dccControl; } -int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int setIndex, int bufferIndexStart, const VideoOutBuffers* buffers, int bufferNum, const VideoOutBufferAttribute2* attribute, int category, void* option) { +int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int setIndex, int bufferIndexStart, const VideoOutBuffers* buffers, int bufferNum, const VideoOutBufferAttribute2* attribute, int category, void* option) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); @@ -88,9 +88,11 @@ int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int setIndex, int bufferIn cfg->buffers[bufferIndexStart + i] = registered[i]; } return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutSubmitChangeBufferAttribute2(int handle, int setIndex, const VideoOutBufferAttribute2* attribute, void* option) { +int APS5_VABI sceVideoOutSubmitChangeBufferAttribute2(int handle, int setIndex, const VideoOutBufferAttribute2* attribute, void* option) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); @@ -120,9 +122,11 @@ int APS5_VABI sceVideoOutSubmitChangeBufferAttribute2(int handle, int setIndex, } cfg->groups[setIndex] = group; return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutUnregisterBuffers(int handle, int setIndex) { +int APS5_VABI sceVideoOutUnregisterBuffers(int handle, int setIndex) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); @@ -148,9 +152,11 @@ int APS5_VABI sceVideoOutUnregisterBuffers(int handle, int setIndex) { } } return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutSubmitFlip(int handle, int index, int flipMode, int64_t flipArg) { +int APS5_VABI sceVideoOutSubmitFlip(int handle, int index, int flipMode, int64_t flipArg) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); @@ -173,6 +179,8 @@ int APS5_VABI sceVideoOutSubmitFlip(int handle, int index, int flipMode, int64_t } lock.unlock(); return VideoOutDriver::Get().SubmitFlip(handle, index, flipMode, flipArg); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } } diff --git a/core/libs/prx/libSceVideoOut/src/Event.cpp b/core/libs/prx/libSceVideoOut/src/Event.cpp index fcf64b9e9..5e3b169bf 100644 --- a/core/libs/prx/libSceVideoOut/src/Event.cpp +++ b/core/libs/prx/libSceVideoOut/src/Event.cpp @@ -107,35 +107,49 @@ static int deleteVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind) { extern "C" { -int APS5_VABI sceVideoOutAddFlipEvent(KernelEqueue eq, int handle, void* udata) { +int APS5_VABI sceVideoOutAddFlipEvent(KernelEqueue eq, int handle, void* udata) try { return registerVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_FLIP, udata); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutAddVblankEvent(KernelEqueue eq, int handle, void* udata) { +int APS5_VABI sceVideoOutAddVblankEvent(KernelEqueue eq, int handle, void* udata) try { return registerVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_VBLANK, udata); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutAddPreVblankStartEvent(KernelEqueue eq, int handle, void* udata) { +int APS5_VABI sceVideoOutAddPreVblankStartEvent(KernelEqueue eq, int handle, void* udata) try { static_cast(eq); static_cast(udata); VideoOutDriver::Get().GetConfig(handle); throw std::runtime_error("VideoOut: physical pre-vblank timing is not implemented"); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutAddOutputModeEvent(KernelEqueue eq, int handle, void* udata) { +int APS5_VABI sceVideoOutAddOutputModeEvent(KernelEqueue eq, int handle, void* udata) try { return registerVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_SET_MODE, udata); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutDeleteFlipEvent(KernelEqueue eq, int handle) { +int APS5_VABI sceVideoOutDeleteFlipEvent(KernelEqueue eq, int handle) try { return deleteVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_FLIP); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutDeleteVblankEvent(KernelEqueue eq, int handle) { +int APS5_VABI sceVideoOutDeleteVblankEvent(KernelEqueue eq, int handle) try { return deleteVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_VBLANK); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutDeletePreVblankStartEvent(KernelEqueue eq, int handle) { +int APS5_VABI sceVideoOutDeletePreVblankStartEvent(KernelEqueue eq, int handle) try { return deleteVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_PRE_VBLANK_START); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } int APS5_VABI sceVideoOutGetEventId(const KernelEvent* ev) { diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index c859772ca..e1038cff4 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -29,7 +29,7 @@ static int validateOutputConfig(int handle, uint64_t mode, const VideoOutOutputO extern "C" { -int APS5_VABI sceVideoOutOpen(int userId, int busType, int index, const void* param) { +int APS5_VABI sceVideoOutOpen(int userId, int busType, int index, const void* param) try { if (param != nullptr) { throw std::runtime_error(std::string(__func__) + ": param not implemented"); } @@ -47,13 +47,17 @@ int APS5_VABI sceVideoOutOpen(int userId, int busType, int index, const void* pa throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_RESOURCE_BUSY"); } return handle; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutClose(int handle) { +int APS5_VABI sceVideoOutClose(int handle) try { return VideoOutDriver::Get().Close(handle) ? 0 : VIDEO_OUT_ERROR_INVALID_HANDLE; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutSetFlipRate(int handle, int rate) { +int APS5_VABI sceVideoOutSetFlipRate(int handle, int rate) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); @@ -65,9 +69,11 @@ int APS5_VABI sceVideoOutSetFlipRate(int handle, int rate) { cfg->Check(); cfg->flipRate = rate; return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutGetFlipStatus(int handle, VideoOutFlipStatus* status) { +int APS5_VABI sceVideoOutGetFlipStatus(int handle, VideoOutFlipStatus* status) try { if (status == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS"); } @@ -79,9 +85,11 @@ int APS5_VABI sceVideoOutGetFlipStatus(int handle, VideoOutFlipStatus* status) { cfg->Check(); *status = cfg->flipStatus; return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutGetVblankStatus(int handle, VideoOutVblankStatus* status) { +int APS5_VABI sceVideoOutGetVblankStatus(int handle, VideoOutVblankStatus* status) try { if (status == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS"); } @@ -93,9 +101,11 @@ int APS5_VABI sceVideoOutGetVblankStatus(int handle, VideoOutVblankStatus* statu cfg->Check(); *status = cfg->vblankStatus; return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutGetOutputStatus(int handle, VideoOutOutputStatus* status) { +int APS5_VABI sceVideoOutGetOutputStatus(int handle, VideoOutOutputStatus* status) try { if (status == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS"); } @@ -113,9 +123,11 @@ int APS5_VABI sceVideoOutGetOutputStatus(int handle, VideoOutOutputStatus* statu status->reserved[1] = 0; status->reserved[2] = 0; return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutIsFlipPending(int handle) { +int APS5_VABI sceVideoOutIsFlipPending(int handle) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); @@ -123,9 +135,11 @@ int APS5_VABI sceVideoOutIsFlipPending(int handle) { std::unique_lock lock(cfg->mutex); cfg->Check(); return cfg->flipStatus.flipPendingNum; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutWaitVblank(int handle) { +int APS5_VABI sceVideoOutWaitVblank(int handle) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); @@ -133,12 +147,11 @@ int APS5_VABI sceVideoOutWaitVblank(int handle) { std::unique_lock lock(cfg->mutex); cfg->Check(); const uint64_t count = cfg->vblankStatus.count; - while (cfg->opened && !cfg->failure && cfg->vblankStatus.count == count) { - cfg->vblankCond.wait(lock); - } - if (cfg->failure) std::rethrow_exception(cfg->failure); - if (!cfg->opened || cfg->closing) throw std::runtime_error("sceVideoOutWaitVblank: port closed during wait"); + cfg->vblankCond.wait(lock, cfg->shutdownToken, [&] { return !cfg->opened || cfg->closing || cfg->failure || cfg->vblankStatus.count != count; }); + cfg->Check(); return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } int APS5_VABI sceVideoOutInitializeOutputOptions(VideoOutOutputOptions* options) { @@ -149,15 +162,17 @@ int APS5_VABI sceVideoOutInitializeOutputOptions(VideoOutOutputOptions* options) return 0; } -int APS5_VABI sceVideoOutIsOutputSupported(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reservedPtr, uint64_t reserved) { +int APS5_VABI sceVideoOutIsOutputSupported(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reservedPtr, uint64_t reserved) try { const int result = validateOutputConfig(handle, mode, options, reservedPtr, reserved); if (result != 0) { return result; } return (mode == VIDEO_OUT_OUTPUT_MODE_119_88HZ) ? 0 : 1; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutConfigureOutput(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reservedPtr, uint64_t reserved) { +int APS5_VABI sceVideoOutConfigureOutput(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reservedPtr, uint64_t reserved) try { const int supported = sceVideoOutIsOutputSupported(handle, mode, options, reservedPtr, reserved); if (supported < 0) { return supported; @@ -173,30 +188,38 @@ int APS5_VABI sceVideoOutConfigureOutput(int handle, uint64_t mode, const VideoO cfg->Check(); cfg->outputMode = mode; return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutSetWindowModeMargins(int handle, int top, int bottom) { +int APS5_VABI sceVideoOutSetWindowModeMargins(int handle, int top, int bottom) try { (void)top; (void)bottom; if (!VideoOutDriver::Get().IsOpen(handle)) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); } return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutLatencyControlWaitBeforeInput(int handle) { +int APS5_VABI sceVideoOutLatencyControlWaitBeforeInput(int handle) try { if (!VideoOutDriver::Get().IsOpen(handle)) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); } return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutLatencyMeasureSetStartPoint(int handle, uint32_t point) { +int APS5_VABI sceVideoOutLatencyMeasureSetStartPoint(int handle, uint32_t point) try { (void)point; if (!VideoOutDriver::Get().IsOpen(handle)) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); } return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } int APS5_VABI sceVideoOutColorSettingsSetGamma(VideoOutColorSettings* settings, float gamma) { @@ -210,7 +233,7 @@ int APS5_VABI sceVideoOutColorSettingsSetGamma(VideoOutColorSettings* settings, return 0; } -int APS5_VABI sceVideoOutAdjustColor(int handle, const VideoOutColorSettings* settings) { +int APS5_VABI sceVideoOutAdjustColor(int handle, const VideoOutColorSettings* settings) try { if (settings == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS"); } @@ -220,6 +243,8 @@ int APS5_VABI sceVideoOutAdjustColor(int handle, const VideoOutColorSettings* se } // Output gamma is accepted but not yet applied to presentation. return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } } diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index c2d62978b..3ea9f206a 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -40,7 +40,7 @@ public: : _config(std::move(config)), _index(index), _ticket(ticket) {} void Wait() override { std::unique_lock lock(_config->mutex); - _config->vblankCond.wait(lock, [&] { + _config->vblankCond.wait(lock, _config->shutdownToken, [&] { return _config->failure || _config->closing || !_config->opened || _config->bufferReuse[_index].IsComplete(_ticket); }); @@ -76,13 +76,14 @@ public: if (waitWhenFull) { int waited = 0; while (queue->reservations.load() >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) { + if (cfg->shutdownToken.stop_requested()) throw ProcessShutdown{}; std::this_thread::sleep_for(std::chrono::milliseconds(10)); if (++waited % 500 == 0) std::fprintf(stderr, "[flip] queue full for %d s; waiting (APS5_FLIP_QUEUE_WAIT)\n", waited / 100); } } std::lock_guard queueLock(queue->mutex); if (queue->failure) std::rethrow_exception(queue->failure); - require(!queue->stopping, "flip during shutdown"); + if (queue->stopping || cfg->shutdownToken.stop_requested()) throw ProcessShutdown{}; std::lock_guard lock(cfg->mutex); checkConfig(*cfg); require(queue->reservations.load() < VIDEO_OUT_FLIP_QUEUE_CAPACITY, "flip queue full"); @@ -153,7 +154,7 @@ void FlipRequest::GpuReady(const std::shared_ptr& frameT AgcDriver::PerformanceTimer readiness("VideoOut.Readiness"); std::lock_guard queueLock(queue->mutex); if (queue->failure) std::rethrow_exception(queue->failure); - require(!queue->stopping, "GPU flip during shutdown"); + if (queue->stopping || cfg->shutdownToken.stop_requested()) throw ProcessShutdown{}; std::lock_guard lock(cfg->mutex); checkConfig(*cfg); require(reserved && !ready && !terminal && cfg->generation == generation, "invalid flip readiness transition"); @@ -172,7 +173,7 @@ void FlipRequest::GpuReady(const std::shared_ptr& frameT static const bool syncFlip = std::getenv("APS5_SYNC_FLIP") != nullptr; if (!syncFlip) return; std::unique_lock lock(cfg->mutex); - cfg->vblankCond.wait(lock, [&] { return gpuComplete || cfg->failure || cfg->closing; }); + cfg->vblankCond.wait(lock, cfg->shutdownToken, [&] { return gpuComplete || cfg->failure || cfg->closing; }); checkConfig(*cfg); require(gpuComplete, "flip preparation did not complete on GPU"); } @@ -232,24 +233,21 @@ void VideoOutDriver::Shutdown() { require(std::this_thread::get_id() != presentThread.get_id(), "presentation thread cannot stop itself"); std::lock_guard shutdownLock(shutdownMutex); if (stopped) return; + LibcRequestShutdown_nid_postfix(); { - std::lock_guard lock(mutex); - { - std::lock_guard queueLock(flipQueue->mutex); - flipQueue->stopping = true; - } - for (int handle = 1; handle < VIDEO_OUT_NUM_MAX; ++handle) { - if (outputs[handle]) close(handle); - } + std::lock_guard queueLock(flipQueue->mutex); + flipQueue->stopping = true; } presentThread.request_stop(); vblankThread.request_stop(); flipQueue->changed.notify_all(); presentThread.join(); vblankThread.join(); - if (window.Handle() != nullptr) { - AgcDriverReleaseWindow_nid_postfix(window.Handle()); - window.Destroy(); + { + std::lock_guard lock(mutex); + for (int handle = 1; handle < VIDEO_OUT_NUM_MAX; ++handle) { + if (outputs[handle]) close(handle); + } } SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER); stopped = true; @@ -262,7 +260,7 @@ int VideoOutDriver::Open(int busType) { { std::lock_guard queueLock(flipQueue->mutex); if (flipQueue->failure) std::rethrow_exception(flipQueue->failure); - require(!flipQueue->stopping, "open during shutdown"); + if (flipQueue->stopping || LibcShutdownToken_nid_postfix().stop_requested()) throw ProcessShutdown{}; } const int handle = busType + 1; require(handle > 0 && handle < VIDEO_OUT_NUM_MAX, "invalid output bus"); @@ -288,6 +286,7 @@ int VideoOutDriver::Open(int busType) { bool VideoOutDriver::Close(int handle) { std::lock_guard lock(mutex); + if (LibcShutdownToken_nid_postfix().stop_requested()) throw ProcessShutdown{}; return close(handle); } @@ -322,7 +321,7 @@ std::shared_ptr VideoOutDriver::GetConfig(int handle) { { std::lock_guard lock(flipQueue->mutex); if (flipQueue->failure) std::rethrow_exception(flipQueue->failure); - require(!flipQueue->stopping, "output is shutting down"); + if (flipQueue->stopping || LibcShutdownToken_nid_postfix().stop_requested()) throw ProcessShutdown{}; } std::lock_guard lock(mutex); require(handle > 0 && handle < VIDEO_OUT_NUM_MAX && contexts[handle] != nullptr, "invalid output handle"); @@ -337,6 +336,7 @@ bool VideoOutDriver::IsOpen(int handle) { } int VideoOutDriver::SubmitFlip(int handle, int index, int flipMode, int64_t flipArg) { + if (LibcShutdownToken_nid_postfix().stop_requested()) throw ProcessShutdown{}; // A title that does not pace on flipPendingNum can run ahead of the presenter now that the queue // worker no longer waits per flip; a full queue is the documented error, not a failure. if (flipQueue->reservations.load() >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) return VIDEO_OUT_ERROR_FLIP_QUEUE_FULL; @@ -392,7 +392,7 @@ void VideoOutDriver::processFlip(FlipRequest& req) { require(req.cfg->lastFlipVblank <= std::numeric_limits::max() - interval, "flip interval overflow"); const auto target = req.cfg->lastFlipVblank + interval; timing.Mark("validate"); - req.cfg->vblankCond.wait(lock, [&] { return req.cfg->vblankStatus.count >= target || req.cfg->failure || req.cfg->closing; }); + req.cfg->vblankCond.wait(lock, req.cfg->shutdownToken, [&] { return req.cfg->vblankStatus.count >= target || req.cfg->failure || req.cfg->closing; }); timing.Mark("vblank_wait"); checkConfig(*req.cfg); } @@ -481,7 +481,10 @@ void VideoOutDriver::presentLoop(std::stop_token token) { } SDL_Event event; while (SDL_PollEvent(&event)) { - require(event.type != SDL_QUIT, "window was closed"); + if (event.type == SDL_QUIT) { + LibcRequestExit_nid_postfix(0); + throw ProcessShutdown{}; + } padInput.HandleEvent(event, window); if (window.Handle() != nullptr) mouseInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); } @@ -503,15 +506,7 @@ void VideoOutDriver::presentLoop(std::stop_token token) { } current.reset(); } - std::list> cancelled; - { - std::lock_guard lock(flipQueue->mutex); - cancelled.swap(flipQueue->requests); - } - if (!cancelled.empty()) { - auto error = std::make_exception_ptr(std::runtime_error("VideoOut: pending flip cancelled during shutdown")); - for (auto& request : cancelled) request->Fail(error); - } + } catch (const ProcessShutdown&) { } catch (const std::exception& error) { std::fprintf(stderr, "[videoout] presentation failed: %s\n", error.what()); std::fflush(stderr); @@ -521,6 +516,16 @@ void VideoOutDriver::presentLoop(std::stop_token token) { std::fflush(stderr); std::terminate(); } + current.reset(); + { + std::list> cancelled; + { + std::lock_guard lock(flipQueue->mutex); + cancelled.swap(flipQueue->requests); + } + } + AgcDriverShutdown_nid_postfix(); + window.Destroy(); } void VideoOutDriver::vblankLoop(std::stop_token token) { diff --git a/core/libs/prx/libc/include/Shutdown.hpp b/core/libs/prx/libc/include/Shutdown.hpp index c1ff16604..dc4007098 100644 --- a/core/libs/prx/libc/include/Shutdown.hpp +++ b/core/libs/prx/libc/include/Shutdown.hpp @@ -1,8 +1,16 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_SHUTDOWN_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_SHUTDOWN_HPP +#include + +class ProcessShutdown final {}; + extern "C" void LibcRegisterShutdown_nid_postfix(void (*callback)()); extern "C" void LibcRunShutdown_nid_postfix(); +extern "C" std::stop_token LibcShutdownToken_nid_postfix(); +extern "C" void LibcRequestShutdown_nid_postfix(); +extern "C" void LibcRequestExit_nid_postfix(int code); +extern "C" [[noreturn]] void LibcAwaitExit_nid_postfix(); extern "C" [[noreturn]] void LibcExit_nid_no_patch(int code); #endif diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index 7a8dcc613..0e8a39c0c 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include "prx/libc/include/Shutdown.hpp" #include "prx/libc/include/General.hpp" @@ -22,9 +24,35 @@ std::thread::id shutdownThread; bool shutdownStarted = false; bool shutdownFinished = false; std::exception_ptr shutdownFailure; +std::stop_source shutdownSource; +std::atomic exitRequested{false}; +std::atomic exitStarted{false}; } +extern "C" std::stop_token LibcShutdownToken_nid_postfix() { + return shutdownSource.get_token(); +} + +extern "C" void LibcRequestShutdown_nid_postfix() { + shutdownSource.request_stop(); +} + +extern "C" void LibcRequestExit_nid_postfix(int code) { + if (exitRequested.exchange(true)) return; + LibcRequestShutdown_nid_postfix(); + std::thread([code] { LibcExit_nid_no_patch(code); }).detach(); +} + +extern "C" [[noreturn]] void LibcAwaitExit_nid_postfix() { + if (!exitRequested.load()) throw ProcessShutdown{}; + { + std::lock_guard lock(shutdownMutex); + if (shutdownThread == std::this_thread::get_id()) throw std::runtime_error("libc: exit thread cannot wait for itself"); + } + for (;;) exitRequested.wait(true); +} + extern "C" void LibcRegisterShutdown_nid_postfix(void (*callback)()) { std::lock_guard lock(shutdownMutex); if (callback == nullptr || shutdownStarted) throw std::runtime_error("libc: invalid shutdown registration"); @@ -43,6 +71,7 @@ extern "C" void LibcRunShutdown_nid_postfix() { shutdownThread = std::this_thread::get_id(); auto callbacks = std::move(shutdownCallbacks); lock.unlock(); + LibcRequestShutdown_nid_postfix(); std::exception_ptr error; for (auto it = callbacks.rbegin(); it != callbacks.rend(); ++it) { try { (*it)(); } @@ -68,6 +97,8 @@ extern "C" { } [[noreturn]] void LibcExit_nid_no_patch(int code) { + exitRequested.store(true); + if (exitStarted.exchange(true)) LibcAwaitExit_nid_postfix(); LibcRunShutdown_nid_postfix(); std::exit(code); } diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 53d33430b..5359fcd33 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -355,7 +355,9 @@ void InvalidParameterHandler(const wchar_t*, const wchar_t*, const wchar_t*, uns // Static destruction runs on the thread that ends the process (ExitProcess -> DLL_PROCESS_DETACH), // so its stack names the exit's caller when no libc exit/abort path was taken. struct ExitReporter { - ~ExitReporter() { ReportBacktrace("process exit (static destruction)"); } + ~ExitReporter() { + if (std::getenv("APS5_TRACE_EXIT") != nullptr) ReportBacktrace("process exit (static destruction)"); + } } g_exitReporter; const bool g_crashReportInstalled = [] { From 417b66323f21f2a131dc63b6cfc069ff6d203b35 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Mon, 28 Sep 2026 18:39:17 +0300 Subject: [PATCH 29/46] chore(readme): remove dup progress map info from readme --- README.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7f6c47a63..c47d78215 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # About +Tool for automatic executables porting to Linux and Windows. + +Includes a [relinker](core/relinker) that converts executable to the target system's native format and implementations of [system prx libraries](core/libs/prx) suitable for dynamic linking. No emulation or separate runtime process. + [![libraries](https://boykopovar.github.io/AnyPS5/badge-libraries.svg)](https://boykopovar.github.io/AnyPS5/) [![shaders](https://boykopovar.github.io/AnyPS5/badge-shaders.svg)](https://boykopovar.github.io/AnyPS5/) [![progress map](https://boykopovar.github.io/AnyPS5/progress.svg)](https://boykopovar.github.io/AnyPS5/) * System libraries: percentage of the functions known to the project so far (declared in [core/libs/prx](core/libs/prx)), not of every PS5 system function. The total grows as more functions are declared. -Tool for automatic executables porting to Linux and Windows. - -Includes a [relinker](core/relinker) that converts executable to the target system's native format and implementations of [system prx libraries](core/libs/prx) suitable for dynamic linking. No emulation or separate runtime process. - Releases will be published after the first full successful launch of at least one game. ## Status @@ -20,10 +20,6 @@ The [shader recompiler](core/shader/recompiler/Recompiler.cpp) successfully prod The real game reaches the logo, main menu, and [gameplay](https://gist.github.com/user-attachments/assets/81d28e9b-c237-4545-b2ca-720071129816) with audio. -[Progress](https://boykopovar.github.io/AnyPS5/), generated by [tools/progress.py](tools/progress.py) on every push to `main`: -- **System libraries\***: each block is a [prx library](core/libs/prx) sized by its functions; green is implemented, blue still calls `NotImplemented`. The total only includes functions already declared in the project, not every function of the PS5 firmware. -- **GPU shader instructions**: each block is an instruction encoding of the AMD RDNA 1 + RDNA 2 [instruction list](tools/rdna_isa.txt); green is recognized by the [shader recompiler](core/shader/recompiler). - [Technical debt of the project](docs/TechnicalDebt.md), [code style conventions](docs/CONVENTIONS.md) ## Build From 87633fc4e8243f237b32ddfc3e3132547b2ecb3c Mon Sep 17 00:00:00 2001 From: boykopovar Date: Mon, 28 Sep 2026 18:42:44 +0300 Subject: [PATCH 30/46] chore(tools): move progress.py description from repo to pr --- tools/README.md | 37 ------------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 tools/README.md diff --git a/tools/README.md b/tools/README.md deleted file mode 100644 index b374dc398..000000000 --- a/tools/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Tools - -## progress.py - -Generates the progress map shown in the main [README](../README.md). - -```sh -python3 tools/progress.py -``` - -Requires only Python 3.9+ with the standard library. It writes: - -- `progress.svg`: treemaps for system libraries and GPU shader instructions -- `badge-libraries.svg`, `badge-shaders.svg`: percentage badges -- `index.html`: the map with per-library and per-encoding tables -- `progress.json`: raw numbers and the names of implemented and pending items - -The [Progress workflow](../.github/workflows/progress.yml) runs it on every push to `main` and deploys the output to GitHub Pages, so nothing generated is committed to the repository. - -To compare two runs, for example `main` and a pull request: - -```sh -python3 tools/progress.py --root base -python3 tools/progress.py head -python3 tools/progress.py --compare base/progress.json head/progress.json -``` - -`--root` measures another source tree with the current script. `--compare` prints a Markdown report of implemented, newly declared, reverted and removed items, and prints nothing when progress did not change. The [Progress report workflow](../.github/workflows/progress-report.yml) runs it for every pull request, and [Progress comment](../.github/workflows/progress-comment.yml) posts the report as a single comment on the pull request, updated on every push. It runs as a separate `workflow_run` so pull requests from forks never run with write permissions. - -### What is measured - -- **System libraries**: functions defined in [core/libs/prx](../core/libs/prx). A function is implemented when it no longer calls `NotImplemented_nid_no_patch`. The total only includes functions already declared in the project, not every function of the PS5 firmware. -- **GPU shader instructions**: instructions from [rdna_isa.txt](rdna_isa.txt) that appear in `RdnaOpcode` of the [shader recompiler](../core/shader/recompiler/RdnaDecoder). FLAT opcodes also count for their GLOBAL and SCRATCH forms, since the decoder accepts all three segments. Decoded opcodes missing from AMD's public list are not counted. - -## rdna_isa.txt - -The AMD RDNA 1 + RDNA 2 instruction list (name and primary encoding), extracted from the [AMD Machine-Readable GPU ISA Specification](https://gpuopen.com/machine-readable-isa/) (MIT License). From 1f595d883ad5e5f5dbe4c581400eac1dc8c0cc72 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Mon, 28 Sep 2026 19:29:49 +0300 Subject: [PATCH 31/46] fix(agc): propagate dispatch worker failures to waiting threads --- .../prx/libSceAgcDriver/Execution/src/Driver.cpp | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp index fb2728197..df87df2d3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp @@ -5957,11 +5957,11 @@ private: CaptureTrace::Log("flip frame=%llu submission=%llu offset=%zu batch=%llu unsignaled=%llu", static_cast(frameSerial), static_cast(submission.serial), cursor, static_cast(batchesAtFlip), static_cast(unsignaledAtFlip)); submission.flips.at(cursor)->GpuReady(frame); } else if (opcode == 0x15) { - timed(&WorkerProfile::dispatchMs, [&] { tolerate("dispatch", [&] { dispatch(queue, packet, submission); }); }); + timed(&WorkerProfile::dispatchMs, [&] { dispatch(queue, packet, submission); }); Graphics::Recorder::CountRecordedWork(); finishDispatchPacket(false); } else if (opcode == 0x16) { - timed(&WorkerProfile::dispatchMs, [&] { tolerate("indirect dispatch", [&] { dispatchIndirect(queue, packet, submission); }); }); + timed(&WorkerProfile::dispatchMs, [&] { dispatchIndirect(queue, packet, submission); }); Graphics::Recorder::CountRecordedWork(); finishDispatchPacket(true); } else if (opcode == 0x3c || opcode == 0x93) { @@ -6166,15 +6166,8 @@ private: submission = Submission{}; } catch (...) { const auto error = std::current_exception(); - try { - std::rethrow_exception(error); - } catch (const std::exception& reason) { - std::fprintf(stderr, "[gpu] worker failed: %s\n", reason.what()); - } catch (...) { - std::fprintf(stderr, "[gpu] worker failed with a non-standard exception\n"); - } - std::fflush(stderr); - std::terminate(); + ReportFailure(error); + for (const auto& [offset, flip] : submission.flips) flip->Fail(error); } } }; From 829e8c14aeb522adeeaf424201199bab08a9a99e Mon Sep 17 00:00:00 2001 From: boykopovar Date: Mon, 28 Sep 2026 19:32:43 +0300 Subject: [PATCH 32/46] fix(tests): align agc flip waits and memory synchronization checks --- core/libs/prx/libSceAgcDriver/tests/Flip.cpp | 34 ++++++++++++++------ core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 18 ++++++----- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp index b2030621f..9beb24074 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp @@ -41,6 +41,21 @@ struct State { AgcDriver::FlipInfo last{}; }; +class FlipRelease final { +public: + explicit FlipRelease(std::shared_ptr state) : state(std::move(state)) {} + ~FlipRelease() { Release(); } + void Release() { + { + std::lock_guard lock(state->mutex); + state->block = false; + } + state->changed.notify_all(); + } +private: + std::shared_ptr state; +}; + class Request final : public AgcDriver::IFlipRequest { public: explicit Request(std::shared_ptr value) : state(std::move(value)) { ++state->alive; } @@ -104,24 +119,23 @@ void testFlipAndBoundary() { output->state->block = true; output->state->checkSelfWait = true; } + std::future boundary; + FlipRelease release(output->state); submitFlip(); { std::unique_lock lock(output->state->mutex); check(output->state->changed.wait_for(lock, std::chrono::seconds(5), [&] { return output->state->entered; }), "worker did not reach flip"); check(output->state->last.argument == -0x123456789abcdefLL && output->state->last.index == -2, "decoded flip arguments changed"); } - auto boundary = std::async(std::launch::async, [] { AgcDriverSuspendPoint_nid_postfix(); }); - check(boundary.wait_for(std::chrono::milliseconds(30)) == std::future_status::timeout, "suspend completed before preceding work"); + boundary = std::async(std::launch::async, [] { AgcDriverSuspendPoint_nid_postfix(); }); + check(boundary.wait_for(std::chrono::seconds(5)) == std::future_status::ready, "suspend blocked on preceding work"); + boundary.get(); + check(output->state->ready == 0, "blocked flip completed before release"); AgcDriverUnregisterVideoOutput_nid_postfix(7, output); auto replacement = std::make_shared(); AgcDriverRegisterVideoOutput_nid_postfix(7, replacement); submitFlip(); - { - std::lock_guard lock(output->state->mutex); - output->state->block = false; - } - output->state->changed.notify_all(); - boundary.get(); + release.Release(); AgcDriverWaitIdle_nid_postfix(); check(output->state->ready == 1 && replacement->state->ready == 1, "registration lifetime or FIFO was lost"); check(output->state->failed == 0, "successful request failed"); @@ -140,6 +154,7 @@ void testFlipAndBoundary() { for (auto& producer : producers) producer.join(); for (auto error : errors) if (error) std::rethrow_exception(error); AgcDriverSuspendPoint_nid_postfix(); + AgcDriverWaitIdle_nid_postfix(); check(replacement->state->ready == 201, "concurrent submissions were lost"); AgcDriverUnregisterVideoOutput_nid_postfix(7, replacement); } @@ -151,10 +166,11 @@ void testFailure() { submitFlip(); std::array messages; std::vector waiters; - for (auto& message : messages) waiters.emplace_back([&message] { message = expectFailure([] { AgcDriverSuspendPoint_nid_postfix(); }); }); + for (auto& message : messages) waiters.emplace_back([&message] { message = expectFailure([] { AgcDriverWaitIdle_nid_postfix(); }); }); for (auto& waiter : waiters) waiter.join(); for (auto& message : messages) check(message == "intentional flip failure", "asynchronous failure was lost"); check(expectFailure([] { AgcDriverWaitIdle_nid_postfix(); }) == messages[0], "idle lost flip failure"); + check(expectFailure([] { AgcDriverSuspendPoint_nid_postfix(); }) == messages[0], "suspend lost flip failure"); check(expectFailure([] { submitFlip(); }) == messages[0], "submit lost flip failure"); check(output->state->ready == 0 && output->state->failed == 1, "failed flip was completed successfully"); AgcDriverUnregisterVideoOutput_nid_postfix(7, output); diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index e18b7baf1..34198c47b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -1,6 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" -#include "prx/libSceAgcDriver/Execution/include/MemoryAccessScope.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libc/include/Shutdown.hpp" @@ -308,13 +307,16 @@ void testMemorySynchronization() { std::uint32_t destination = 0; bool read = false; bool written = false; - } memory; + }; + static MemoryState memory; + memory = {}; AgcDriver::QueueState state; - const auto resolve = [](void* context, std::uint64_t address, std::size_t bytes, bool writable) { - auto& memory = *static_cast(context); + struct FlushHookReset { + ~FlushHookReset() { AgcDriver::GuestMemory::SetFlushHook(nullptr); } + } reset; + const auto resolve = [](std::uint64_t address, std::size_t bytes) { check(bytes == sizeof(std::uint32_t), "memory transfer resolved an unrelated range"); - if (writable) { - check(address == reinterpret_cast(&memory.destination), "memory transfer resolved an unrelated destination"); + if (address == reinterpret_cast(&memory.destination)) { memory.written = true; } else { check(address == reinterpret_cast(&memory.source), "memory transfer resolved an unrelated source"); @@ -322,7 +324,7 @@ void testMemorySynchronization() { memory.read = true; } }; - const AgcDriver::GuestMemory::MemoryAccessScope scope(&memory, resolve); + AgcDriver::GuestMemory::SetFlushHook(resolve); execute(state, makePacket(0x37, {0x100, low(&memory.destination), high(&memory.destination), 17})); check(memory.written && !memory.read && memory.destination == 17, "WRITE_DATA did not synchronize its destination"); for (const auto opcode : {0x40u, 0x50u}) { @@ -333,7 +335,7 @@ void testMemorySynchronization() { execute(state, packet); check(memory.read && memory.written && memory.destination == 42, "memory copy used stale data before range synchronization"); } - const AgcDriver::GuestMemory::MemoryAccessScope rejecting(&memory, [](void*, std::uint64_t, std::size_t, bool) { + AgcDriver::GuestMemory::SetFlushHook([](std::uint64_t, std::size_t) { throw std::runtime_error("range synchronization failed"); }); expectFailure([&] { execute(state, makePacket(0x37, {0x100, low(&memory.destination), high(&memory.destination), 99})); }, "range synchronization failed"); From 86401fe5ab08d3eeccc2cfa1941d7db4379b5dee Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Tue, 29 Sep 2026 04:09:09 +1000 Subject: [PATCH 33/46] feat(shader): support scalar 64-bit reverse and arithmetic shift --- .../RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp | 2 ++ core/shader/recompiler/RdnaDecoder/src/RdnaOpcode.cpp | 2 ++ .../recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp | 4 ++++ .../recompiler/Translation/src/ScalarInstructions.cpp | 9 +++++++++ 4 files changed, 17 insertions(+) diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp index f06f8d362..cf39f1598 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp @@ -21,6 +21,7 @@ enum class RdnaOpcode : std::uint16_t { SLshlB32, SLshrB32, SAshrI32, + SAshrI64, SBfeU32, SBfeI32, SCmpEqI32, @@ -119,6 +120,7 @@ enum class RdnaOpcode : std::uint16_t { SAbsI32, SAbsdiffI32, SBrevB32, + SBrevB64, SSextI32I8, SSextI32I16, SBcnt0I32B32, diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaOpcode.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaOpcode.cpp index 54ecc199c..edf534d9e 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaOpcode.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaOpcode.cpp @@ -30,6 +30,7 @@ bool IsScalarAluOpcode(RdnaOpcode opcode) { case RdnaOpcode::SLshlB32: case RdnaOpcode::SLshrB32: case RdnaOpcode::SAshrI32: + case RdnaOpcode::SAshrI64: case RdnaOpcode::SBfeU32: case RdnaOpcode::SBfeI32: case RdnaOpcode::SCmpEqI32: @@ -41,6 +42,7 @@ bool IsScalarAluOpcode(RdnaOpcode opcode) { case RdnaOpcode::SAbsI32: case RdnaOpcode::SAbsdiffI32: case RdnaOpcode::SBrevB32: + case RdnaOpcode::SBrevB64: case RdnaOpcode::SSextI32I8: case RdnaOpcode::SSextI32I16: case RdnaOpcode::SBcnt0I32B32: diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp index b93ed8afb..1a8242074 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp @@ -18,6 +18,7 @@ RdnaOpcode decodeSop1Opcode(std::uint32_t opcode) { case 0x09u: return RdnaOpcode::SWqmB32; case 0x0au: return RdnaOpcode::SWqmB64; case 0x0bu: return RdnaOpcode::SBrevB32; + case 0x0cu: return RdnaOpcode::SBrevB64; case 0x0du: return RdnaOpcode::SBcnt0I32B32; case 0x0fu: return RdnaOpcode::SBcnt1I32B32; case 0x10u: return RdnaOpcode::SBcnt1I32B64; @@ -85,6 +86,7 @@ RdnaOpcode decodeSop2Opcode(std::uint32_t opcode) { case 0x20u: return RdnaOpcode::SLshrB32; case 0x21u: return RdnaOpcode::SLshrB64; case 0x22u: return RdnaOpcode::SAshrI32; + case 0x23u: return RdnaOpcode::SAshrI64; case 0x24u: return RdnaOpcode::SBfmB32; case 0x25u: return RdnaOpcode::SBfmB64; case 0x26u: return RdnaOpcode::SMulI32; @@ -197,6 +199,8 @@ std::uint32_t scalarDestinationDwordCount(RdnaOpcode opcode) { case RdnaOpcode::SMovB64: case RdnaOpcode::SCmovB64: case RdnaOpcode::SNotB64: + case RdnaOpcode::SBrevB64: + case RdnaOpcode::SAshrI64: case RdnaOpcode::SWqmB64: case RdnaOpcode::SBitset0B64: case RdnaOpcode::SBitset1B64: diff --git a/core/shader/recompiler/Translation/src/ScalarInstructions.cpp b/core/shader/recompiler/Translation/src/ScalarInstructions.cpp index 3515a3132..6505fea40 100644 --- a/core/shader/recompiler/Translation/src/ScalarInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ScalarInstructions.cpp @@ -203,6 +203,13 @@ bool TranslationContext::emitScalar(const RdnaInstruction& inst) { return simpleInteger(inst, IrOpcode::BitwiseNot32, IrType::U32, false, false, true); case RdnaOpcode::SBrevB32: return simpleInteger(inst, IrOpcode::BitReverse32, IrType::U32, false, false, false); + case RdnaOpcode::SBrevB64: { + const auto source = readU32Pair(sourceAt(inst, 0u)); + const IrU32 low(ir.Emit(IrOpcode::BitReverse32, IrType::U32, {&source[1].Value()})); + const IrU32 high(ir.Emit(IrOpcode::BitReverse32, IrType::U32, {&source[0].Value()})); + writeU32Pair(inst.destination, {low, high}); + return true; + } case RdnaOpcode::SSextI32I8: case RdnaOpcode::SSextI32I16: { const auto source = readU32(sourceAt(inst, 0u)); @@ -235,6 +242,8 @@ bool TranslationContext::emitScalar(const RdnaInstruction& inst) { return simpleInteger(inst, IrOpcode::ShiftRightLogical32, IrType::U32, false, true, true); case RdnaOpcode::SAshrI32: return simpleInteger(inst, IrOpcode::ShiftRightArithmetic32, IrType::U32, false, true, true); + case RdnaOpcode::SAshrI64: + return simpleInteger(inst, IrOpcode::ShiftRightArithmetic64, IrType::U64, false, false, true); case RdnaOpcode::SLshlB64: return simpleInteger(inst, IrOpcode::ShiftLeftLogical64, IrType::U64, false, false, true); case RdnaOpcode::SLshrB64: From 00261033fab0dd3d0c00ad97b3ad1eb2ea32f795 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Mon, 28 Sep 2026 22:58:17 +0300 Subject: [PATCH 34/46] refactor(docs): separated user and dev in docs + compatibility file --- README.md | 8 ++++-- core/libs/prx/libcohtml/DEPLOY.md | 2 +- core/libs/prx/libfmod/Export.cpp | 2 +- docs/TechnicalDebt.md | 41 ------------------------------- docs/{ => dev}/CONVENTIONS.md | 0 docs/dev/TechnicalDebt.md | 41 +++++++++++++++++++++++++++++++ docs/user/COMPATIBILITY.md | 5 ++++ docs/{ => user}/INPUT_MAPPING.md | 0 8 files changed, 54 insertions(+), 45 deletions(-) delete mode 100644 docs/TechnicalDebt.md rename docs/{ => dev}/CONVENTIONS.md (100%) create mode 100644 docs/dev/TechnicalDebt.md create mode 100644 docs/user/COMPATIBILITY.md rename docs/{ => user}/INPUT_MAPPING.md (100%) diff --git a/README.md b/README.md index 8a780366b..47e39fe68 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The [shader recompiler](core/shader/recompiler/Recompiler.cpp) successfully prod The real game reaches the logo, main menu, and [gameplay](https://gist.github.com/user-attachments/assets/81d28e9b-c237-4545-b2ca-720071129816) with audio. -[Technical debt of the project](docs/TechnicalDebt.md), [code style conventions](docs/CONVENTIONS.md) +[Technical debt of the project](docs/dev/TechnicalDebt.md), [code style conventions](docs/dev/CONVENTIONS.md) ## Build @@ -30,9 +30,13 @@ The relinker uses only the C++20 standard library and should build with any conf The project targets maximum compiler portability. Support for additional compilers will be addressed after the first successful game launch. +## Compatibility + +See the [game compatibility list](docs/user/COMPATIBILITY.md) for tested games and known issues. + ## Input mapping -SDL-mapped game controllers are supported, including analog sticks and triggers. Keyboard and mouse controls can be configured with an `anyps5-input.ini` file. See [input mapping](docs/INPUT_MAPPING.md) for the supported devices and configuration format. +SDL-mapped game controllers are supported, including analog sticks and triggers. Keyboard and mouse controls can be configured with an `anyps5-input.ini` file. See [input mapping](docs/user/INPUT_MAPPING.md) for the supported devices and configuration format. ## Disclaimer diff --git a/core/libs/prx/libcohtml/DEPLOY.md b/core/libs/prx/libcohtml/DEPLOY.md index de4c3ffea..0b4cbdc5d 100644 --- a/core/libs/prx/libcohtml/DEPLOY.md +++ b/core/libs/prx/libcohtml/DEPLOY.md @@ -5,7 +5,7 @@ Game `DT_NEEDED` expects exactly: `libcohtml.Prospero.prx`. Copy next to the relinked eboot (same directory): - `build/core/libs/libs/libcohtml.Prospero.prx` (patched build output, 50 NIDs) — required. -- `libgcc_s_seh-1.dll`, `libstdc++-6.dll`, `libwinpthread-1.dll` from `winlibs-gcc15/mingw64/bin` — required on Windows (see `docs/TechnicalDebt.md`). +- `libgcc_s_seh-1.dll`, `libstdc++-6.dll`, `libwinpthread-1.dll` from `winlibs-gcc15/mingw64/bin` — required on Windows (see [technical debt](../../../../docs/dev/TechnicalDebt.md)). Do NOT deploy: diff --git a/core/libs/prx/libfmod/Export.cpp b/core/libs/prx/libfmod/Export.cpp index 6c5a7476f..bd7545a64 100644 --- a/core/libs/prx/libfmod/Export.cpp +++ b/core/libs/prx/libfmod/Export.cpp @@ -1635,7 +1635,7 @@ public: } else { // Compressed (Vorbis/MP3/FSB/AT9...) or unknown: audible // placeholder so the mixer path can be heard. Real decoders - // (Vorbis/MP3/AT9) remain future work — see docs/TechnicalDebt. + // (Vorbis/MP3/AT9) remain future work — see docs/dev/TechnicalDebt. FmodMakeSine(created->pcm, kFmodOutRate, 2, 440.0f, 1.0f); created->pcmRate = kFmodOutRate; created->pcmCh = 2; diff --git a/docs/TechnicalDebt.md b/docs/TechnicalDebt.md deleted file mode 100644 index 3b2246721..000000000 --- a/docs/TechnicalDebt.md +++ /dev/null @@ -1,41 +0,0 @@ -# Project technical debt - -### Build - -- Building on Windows requires a specific version of mingw - MinGW-w64 GCC 15.2.0 (`winlibs-gcc15`, `x86_64-ucrt-posix-seh`) -- Even compiled prx libraries on Windows require nearby (static linking of these dependencies causes conflicts): - - libgcc_s_seh-1.dll - - libstdc++-6.dll - - libwinpthread-1.dll - -### Silent stubs - -Throughout the project, every function at every stage either **does exactly what it's supposed to or throws an exception**. Everywhere... except: -- [libSceSaveDataDialog.native](../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) -- [libSceCommonDialog](../core/libs/prx/libSceCommonDialog/Export.cpp) -- The shader recompiler [skips baryctric coordinates](../core/shader/recompiler/Recompiler.cpp) (is not even passed to SpirvTargetOptions at row 212). - -### Unknown function info - -- [zARR5aCmkoY](../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - unknown name, signature -- [qj7QZpgr9Uw](../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - unknown name -- [fd5Bp5tGTgo](../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - unknown name -- [dolOmWH+huQ](../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - unknown name -- [V++UgBtQhn0](../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - unknown name -- [gQkqkLttcpw](../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - unknown name, signature -- [sceKernelInternalMemoryGetModuleSegmentInfo](../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - unknown signature -- [sceLibcInternalBacktraceForGame](../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature -- [sceLibcInternalHeapErrorReportForGame](../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature -- [__progname](../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export - -### Functional - -- [Shader recompilation](../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../core/shader/recompiler) was written completely independently from [libs/prx](../core/libs/prx). -- The executable file that [relinker](../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens the console when launched, which is inconvenient for playability. -- [Relinker](../core/relinker/elfpatcher/src) doesn't add an icon to the generated executable. This should be done without adding dependencies (only standard). -- `--to-intel` does not lower the register form of INSERTQ (`F2 0F 79`) nor MONITORX/MWAITX/CLZERO/RDPRU/MCOMMIT; the [lowering](../core/relinker/codegen/src/x86/Sse4aLowering.cpp) fails the relink instead. SHA-NI is not substituted. -- The length-changing path of the [instruction rewriter](../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust VEX/0F38/0F3A RIP-relative operands, data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. -- The Linux placement of the `--to-intel` stubs in the [ELF patcher](../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. -- The libc [SSE4a trap emulation](../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. -- `--to-intel` lowerings that need a stub section are rejected for `sce_module` guest modules: the [guest module builder](../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) does not pass stub sites to the [guest module writer](../core/relinker/elfpatcher/include/elfpatcher/general/GuestModuleWriter.hpp). -- The game can expect its modified prx from the `sce_module`/`sce_modules` folder - this support is not implemented. diff --git a/docs/CONVENTIONS.md b/docs/dev/CONVENTIONS.md similarity index 100% rename from docs/CONVENTIONS.md rename to docs/dev/CONVENTIONS.md diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md new file mode 100644 index 000000000..8468605e0 --- /dev/null +++ b/docs/dev/TechnicalDebt.md @@ -0,0 +1,41 @@ +# Project technical debt + +### Build + +- Building on Windows requires a specific version of mingw - MinGW-w64 GCC 15.2.0 (`winlibs-gcc15`, `x86_64-ucrt-posix-seh`) +- Even compiled prx libraries on Windows require nearby (static linking of these dependencies causes conflicts): + - libgcc_s_seh-1.dll + - libstdc++-6.dll + - libwinpthread-1.dll + +### Silent stubs + +Throughout the project, every function at every stage either **does exactly what it's supposed to or throws an exception**. Everywhere... except: +- [libSceSaveDataDialog.native](../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) +- [libSceCommonDialog](../../core/libs/prx/libSceCommonDialog/Export.cpp) +- The shader recompiler [skips baryctric coordinates](../../core/shader/recompiler/Recompiler.cpp) (is not even passed to SpirvTargetOptions at row 212). + +### Unknown function info + +- [zARR5aCmkoY](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - unknown name, signature +- [qj7QZpgr9Uw](../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - unknown name +- [fd5Bp5tGTgo](../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - unknown name +- [dolOmWH+huQ](../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - unknown name +- [V++UgBtQhn0](../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - unknown name +- [gQkqkLttcpw](../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - unknown name, signature +- [sceKernelInternalMemoryGetModuleSegmentInfo](../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - unknown signature +- [sceLibcInternalBacktraceForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature +- [sceLibcInternalHeapErrorReportForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature +- [__progname](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export + +### Functional + +- [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx). +- The executable file that [relinker](../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens the console when launched, which is inconvenient for playability. +- [Relinker](../../core/relinker/elfpatcher/src) doesn't add an icon to the generated executable. This should be done without adding dependencies (only standard). +- `--to-intel` does not lower the register form of INSERTQ (`F2 0F 79`) nor MONITORX/MWAITX/CLZERO/RDPRU/MCOMMIT; the [lowering](../../core/relinker/codegen/src/x86/Sse4aLowering.cpp) fails the relink instead. SHA-NI is not substituted. +- The length-changing path of the [instruction rewriter](../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust VEX/0F38/0F3A RIP-relative operands, data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. +- The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. +- The libc [SSE4a trap emulation](../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. +- `--to-intel` lowerings that need a stub section are rejected for `sce_module` guest modules: the [guest module builder](../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) does not pass stub sites to the [guest module writer](../../core/relinker/elfpatcher/include/elfpatcher/general/GuestModuleWriter.hpp). +- The game can expect its modified prx from the `sce_module`/`sce_modules` folder - this support is not implemented. diff --git a/docs/user/COMPATIBILITY.md b/docs/user/COMPATIBILITY.md new file mode 100644 index 000000000..45a370021 --- /dev/null +++ b/docs/user/COMPATIBILITY.md @@ -0,0 +1,5 @@ +# Compatibility + +| Game | ID | Windows | Linux | GTX 1050 Ti / i5-7500 | +|----------------|-----------|-------------------|-------|-----------------------| +| Dreaming Sarah | PPSA02929 | In game, playable | ? | 60 FPS | diff --git a/docs/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md similarity index 100% rename from docs/INPUT_MAPPING.md rename to docs/user/INPUT_MAPPING.md From 46382612cb475f268fc97380ec538a44eb11c52b Mon Sep 17 00:00:00 2001 From: boykopovar Date: Mon, 28 Sep 2026 23:15:22 +0300 Subject: [PATCH 35/46] fix(libcohtml): fix build libcohtml with target libs o linux --- core/libs/CMakeLists.txt | 11 +++++++++++ core/libs/prx/libcohtml/CMakeLists.txt | 13 ------------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index aa3c058ff..9a64bfbe7 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -107,6 +107,17 @@ foreach(LIB_DIR ${LIBS_DIRS}) endif() endforeach() +set(LIBCOHTML_PATCHED ${PATCHED_DIR}/libcohtml.prx) +set(LIBCOHTML_PROSPERO_PATCHED ${PATCHED_DIR}/libcohtml.Prospero.prx) +add_custom_command( + OUTPUT ${LIBCOHTML_PROSPERO_PATCHED} + COMMAND ${CMAKE_COMMAND} -E copy "${LIBCOHTML_PATCHED}" "${LIBCOHTML_PROSPERO_PATCHED}" + DEPENDS ${LIBCOHTML_PATCHED} + VERBATIM +) +add_custom_target(libcohtml_prospero DEPENDS ${LIBCOHTML_PROSPERO_PATCHED}) +list(APPEND PATCHED_OUTPUTS ${LIBCOHTML_PROSPERO_PATCHED}) + add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) diff --git a/core/libs/prx/libcohtml/CMakeLists.txt b/core/libs/prx/libcohtml/CMakeLists.txt index f32848eb2..842799d34 100644 --- a/core/libs/prx/libcohtml/CMakeLists.txt +++ b/core/libs/prx/libcohtml/CMakeLists.txt @@ -4,16 +4,3 @@ add_library(libcohtml SHARED EXCLUDE_FROM_ALL target_include_directories(libcohtml PRIVATE ${LIBS_INCLUDE_DIR}) set_target_properties(libcohtml PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(libcohtml) - -set(LIBCOHTML_PATCHED ${PATCHED_DIR}/libcohtml.prx) -set(LIBCOHTML_PROSPERO_PATCHED ${PATCHED_DIR}/libcohtml.Prospero.prx) -add_custom_command( - OUTPUT ${LIBCOHTML_PROSPERO_PATCHED} - COMMAND ${CMAKE_COMMAND} -E copy ${LIBCOHTML_PATCHED} ${LIBCOHTML_PROSPERO_PATCHED} - DEPENDS ${LIBCOHTML_PATCHED} - COMMENT "Providing libcohtml.Prospero.prx for DT_NEEDED" - VERBATIM -) -add_custom_target(libcohtml_prospero DEPENDS ${LIBCOHTML_PROSPERO_PATCHED}) -list(APPEND PATCHED_OUTPUTS ${LIBCOHTML_PROSPERO_PATCHED}) -set(PATCHED_OUTPUTS ${PATCHED_OUTPUTS} PARENT_SCOPE) From 1071c9e09c1400d11a32f9b57b9c95ef240797db Mon Sep 17 00:00:00 2001 From: boykopovar Date: Tue, 29 Sep 2026 00:15:21 +0300 Subject: [PATCH 36/46] feat(ci): add release workflow + update readme --- .github/workflows/release.yml | 111 ++++++++++++++++++++++++++++++++++ README.md | 16 ++--- docs/user/COMPATIBILITY.md | 8 +-- tools/package_release.py | 66 ++++++++++++++++++++ tools/progress.py | 2 +- 5 files changed, 190 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 tools/package_release.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..04afa2cd8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,111 @@ +name: Release + +on: + push: + tags: ['v*'] + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +env: + CONFIGURE: >- + -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON + -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ + -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + WINLIBS_RELEASE: 15.2.0posix-14.0.0-ucrt-r7 + WINLIBS_ZIP: winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip + +jobs: + build: + name: Build and test (${{ matrix.platform }}) + strategy: + fail-fast: true + matrix: + include: + - platform: linux + runner: ubuntu-latest + timeout: 30 + - platform: windows + runner: windows-latest + timeout: 45 + runs-on: ${{ matrix.runner }} + timeout-minutes: ${{ matrix.timeout }} + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v7 + with: + submodules: true + - id: toolchain + if: matrix.platform == 'windows' + uses: actions/cache@v6 + with: + path: C:\winlibs + key: ${{ env.WINLIBS_ZIP }} + - if: matrix.platform == 'windows' && steps.toolchain.outputs.cache-hit != 'true' + run: | + curl -sSfL -o "$RUNNER_TEMP/winlibs.zip" "https://github.com/brechtsanders/winlibs_mingw/releases/download/$WINLIBS_RELEASE/$WINLIBS_ZIP" + 7z x "$RUNNER_TEMP/winlibs.zip" -oC:/winlibs + - if: matrix.platform == 'windows' + run: echo 'C:\winlibs\mingw64\bin' >> "$GITHUB_PATH" + - uses: hendrikmuhs/ccache-action@v1.2.24 + with: + key: ${{ matrix.platform }} + save: false + - name: Configure Linux + if: matrix.platform == 'linux' + run: cmake -S . -B build $CONFIGURE + - name: Configure Windows + if: matrix.platform == 'windows' + shell: cmd + run: cmake -S . -B build %CONFIGURE% + - name: Build project and tests + run: cmake --build build + - name: Run tests + run: ctest --test-dir build --output-on-failure --timeout 120 --stop-on-failure --no-tests=error + - name: Build patched libraries + run: cmake --build build --target libs + - name: Package release files + run: python tools/package_release.py --platform "${{ matrix.platform }}" --build build --output dist + - uses: actions/upload-artifact@v7 + with: + name: release-${{ matrix.platform }} + path: dist/* + if-no-files-found: error + compression-level: 0 + + publish: + name: Publish release + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RELEASE_TAG: ${{ github.ref_name }} + steps: + - uses: actions/checkout@v7 + - uses: actions/download-artifact@v8 + with: + pattern: release-* + merge-multiple: true + path: dist + - name: Check release files and collect documentation + shell: bash + run: | + for file in prx-windows.zip prx-windows.tar.gz prx-linux.zip prx-linux.tar.gz relinker.exe relinker; do + test -s "dist/$file" + done + python tools/package_release.py --docs docs/user --output dist + chmod +x dist/relinker + - name: Create draft and upload files + run: gh release create "$RELEASE_TAG" dist/* --verify-tag --draft --title "$RELEASE_TAG" --generate-notes + - name: Publish complete release + run: gh release edit "$RELEASE_TAG" --draft=false diff --git a/README.md b/README.md index 47e39fe68..4b658c471 100644 --- a/README.md +++ b/README.md @@ -4,21 +4,21 @@ Tool for automatic executables porting to Linux and Windows. Includes a [relinker](core/relinker) that converts executable to the target system's native format and implementations of [system prx libraries](core/libs/prx) suitable for dynamic linking. No emulation or separate runtime process. + + +## Status + [![libraries](https://boykopovar.github.io/AnyPS5/badge-libraries.svg)](https://boykopovar.github.io/AnyPS5/) [![shaders](https://boykopovar.github.io/AnyPS5/badge-shaders.svg)](https://boykopovar.github.io/AnyPS5/) [![progress map](https://boykopovar.github.io/AnyPS5/progress.svg)](https://boykopovar.github.io/AnyPS5/) -* System libraries: percentage of the functions known to the project so far (declared in [core/libs/prx](core/libs/prx)), not of every PS5 system function. The total grows as more functions are declared. +[List of verified games](docs/user/COMPATIBILITY.md) -Releases will be published after the first full successful launch of at least one game. +Dreaming Sarah (2D platformer) runs at a stable 60 fps on a GTX 1050 Ti / i5-7500 3.4GHz. -## Status +Unsupported or unexpected states strictly throw `std::runtime_error`. `what()` is printed to stderr and the process terminates. -Execution reaches `_start`, [stack unwinding](core/libs/prx/libc/src/exception/Unwind.cpp) and exception handling tables are built, reaches main. Unsupported or unexpected states strictly throw `std::runtime_error`. `what()` is printed to stderr and the process terminates. - -The [shader recompiler](core/shader/recompiler/Recompiler.cpp) successfully produces SPIR-V, validated via [Spirv-Tools](3rdparty/SPIRV-Tools) when built with `ANYPS5_ENABLE_SPIRV_TOOLS`. - -The real game reaches the logo, main menu, and [gameplay](https://gist.github.com/user-attachments/assets/81d28e9b-c237-4545-b2ca-720071129816) with audio. +The [shader recompiler](core/shader/recompiler/Recompiler.cpp) successfully produces SPIR-V (validated via [Spirv-Tools](3rdparty/SPIRV-Tools) when built with `ANYPS5_ENABLE_SPIRV_TOOLS`). [Technical debt of the project](docs/dev/TechnicalDebt.md), [code style conventions](docs/dev/CONVENTIONS.md) diff --git a/docs/user/COMPATIBILITY.md b/docs/user/COMPATIBILITY.md index 45a370021..4b62cc293 100644 --- a/docs/user/COMPATIBILITY.md +++ b/docs/user/COMPATIBILITY.md @@ -1,5 +1,5 @@ -# Compatibility +# Tested compatibility -| Game | ID | Windows | Linux | GTX 1050 Ti / i5-7500 | -|----------------|-----------|-------------------|-------|-----------------------| -| Dreaming Sarah | PPSA02929 | In game, playable | ? | 60 FPS | +| Game | ID | Windows | Linux | GTX 1050 Ti / i5-7500 3.4GHz | Intel(R) HD Graphic 620 / i5-7200 2.5GHz | +|----------------|-----------|-------------------|-------|------------------------------|------------------------------------------| +| Dreaming Sarah | PPSA02929 | In game, playable | ? | 60 FPS | 36 FPS | diff --git a/tools/package_release.py b/tools/package_release.py new file mode 100644 index 000000000..c8b7d0c68 --- /dev/null +++ b/tools/package_release.py @@ -0,0 +1,66 @@ +import argparse +import shutil +import tarfile +import zipfile +from pathlib import Path + + +def package(platform, build, output): + libraries = sorted((build / "core/libs/libs").glob("*.prx")) + expected = {f"{directory.name}.prx" for directory in Path("core/libs/prx").iterdir() if directory.is_dir()} + expected.add("libcohtml.Prospero.prx") + missing = expected - {library.name for library in libraries} + if missing: + raise RuntimeError(f"Missing patched libraries: {', '.join(sorted(missing))}") + executable = "relinker.exe" if platform == "windows" else "relinker" + files = list(libraries) + if platform == "windows": + runtime = Path("C:/winlibs/mingw64/bin") + files.extend(runtime / name for name in ("libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll")) + binary = build / "core/relinker" / executable + for file in [*files, binary]: + if not file.is_file() or file.stat().st_size == 0: + raise RuntimeError(f"Missing or empty release file: {file}") + output.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(output / f"prx-{platform}.zip", "w", compression=zipfile.ZIP_DEFLATED) as archive: + for file in files: + archive.write(file, arcname=file.name) + with tarfile.open(output / f"prx-{platform}.tar.gz", "w:gz") as archive: + for file in files: + archive.add(file, arcname=file.name) + shutil.copy2(binary, output / executable) + + +def collect_docs(source, output): + documents = sorted(source.rglob("*.md")) + if not documents: + raise RuntimeError(f"No Markdown documents found in {source}") + names = set() + for document in documents: + if document.name in names or (output / document.name).exists(): + raise RuntimeError(f"Duplicate release asset name: {document.name}") + names.add(document.name) + output.mkdir(parents=True, exist_ok=True) + for document in documents: + shutil.copy2(document, output / document.name) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--platform", choices=("linux", "windows")) + parser.add_argument("--build", type=Path) + parser.add_argument("--docs", type=Path) + parser.add_argument("--output", required=True, type=Path) + args = parser.parse_args() + if args.docs is not None: + if args.platform is not None or args.build is not None: + parser.error("--docs cannot be combined with --platform or --build") + collect_docs(args.docs, args.output) + else: + if args.platform is None or args.build is None: + parser.error("--platform and --build are required when --docs is not specified") + package(args.platform, args.build, args.output) + + +if __name__ == "__main__": + main() diff --git a/tools/progress.py b/tools/progress.py index 2af9611d1..2af716e40 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -26,7 +26,7 @@ OPCODE_ALIASES = { } REPORT_ROWS = 100 PANEL_WIDTH, GAP, MAP_HEIGHT, HEADER = 495, 10, 280, 30 -DONE_COLOR, TODO_COLOR, BORDER, TEXT = "#2ea043", "#1f6feb", "#0d1117", "#ffffff" +DONE_COLOR, TODO_COLOR, BORDER, TEXT = "#2ea043", "#6e7681", "#0d1117", "#ffffff" def body_end(text, start): From 1bc7f0151148c85a6d673cee4837ad2f36562f5c Mon Sep 17 00:00:00 2001 From: boykopovar Date: Tue, 29 Sep 2026 00:22:44 +0300 Subject: [PATCH 37/46] feat(ci): add version to release file names --- .github/workflows/release.yml | 8 ++++---- tools/package_release.py | 23 ++++++++++++++--------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 04afa2cd8..aa0ec6803 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,7 @@ concurrency: cancel-in-progress: false env: + RELEASE_TAG: ${{ github.ref_name }} CONFIGURE: >- -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ @@ -71,7 +72,7 @@ jobs: - name: Build patched libraries run: cmake --build build --target libs - name: Package release files - run: python tools/package_release.py --platform "${{ matrix.platform }}" --build build --output dist + run: python tools/package_release.py --platform "${{ matrix.platform }}" --build build --output dist --version "$RELEASE_TAG" - uses: actions/upload-artifact@v7 with: name: release-${{ matrix.platform }} @@ -89,7 +90,6 @@ jobs: env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} - RELEASE_TAG: ${{ github.ref_name }} steps: - uses: actions/checkout@v7 - uses: actions/download-artifact@v8 @@ -100,11 +100,11 @@ jobs: - name: Check release files and collect documentation shell: bash run: | - for file in prx-windows.zip prx-windows.tar.gz prx-linux.zip prx-linux.tar.gz relinker.exe relinker; do + for file in "prx-windows-$RELEASE_TAG.zip" "prx-windows-$RELEASE_TAG.tar.gz" "prx-linux-$RELEASE_TAG.zip" "prx-linux-$RELEASE_TAG.tar.gz" "relinker-$RELEASE_TAG.exe" "relinker-$RELEASE_TAG"; do test -s "dist/$file" done python tools/package_release.py --docs docs/user --output dist - chmod +x dist/relinker + chmod +x "dist/relinker-$RELEASE_TAG" - name: Create draft and upload files run: gh release create "$RELEASE_TAG" dist/* --verify-tag --draft --title "$RELEASE_TAG" --generate-notes - name: Publish complete release diff --git a/tools/package_release.py b/tools/package_release.py index c8b7d0c68..86209fb60 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -1,11 +1,14 @@ import argparse +import re import shutil import tarfile import zipfile from pathlib import Path -def package(platform, build, output): +def package(platform, build, output, version): + if not re.fullmatch(r"v[0-9A-Za-z][0-9A-Za-z._-]*", version) or version.endswith("."): + raise ValueError(f"Invalid release tag for asset filenames: {version}") libraries = sorted((build / "core/libs/libs").glob("*.prx")) expected = {f"{directory.name}.prx" for directory in Path("core/libs/prx").iterdir() if directory.is_dir()} expected.add("libcohtml.Prospero.prx") @@ -22,13 +25,14 @@ def package(platform, build, output): if not file.is_file() or file.stat().st_size == 0: raise RuntimeError(f"Missing or empty release file: {file}") output.mkdir(parents=True, exist_ok=True) - with zipfile.ZipFile(output / f"prx-{platform}.zip", "w", compression=zipfile.ZIP_DEFLATED) as archive: + with zipfile.ZipFile(output / f"prx-{platform}-{version}.zip", "w", compression=zipfile.ZIP_DEFLATED) as archive: for file in files: archive.write(file, arcname=file.name) - with tarfile.open(output / f"prx-{platform}.tar.gz", "w:gz") as archive: + with tarfile.open(output / f"prx-{platform}-{version}.tar.gz", "w:gz") as archive: for file in files: archive.add(file, arcname=file.name) - shutil.copy2(binary, output / executable) + asset = f"relinker-{version}.exe" if platform == "windows" else f"relinker-{version}" + shutil.copy2(binary, output / asset) def collect_docs(source, output): @@ -49,17 +53,18 @@ def main(): parser = argparse.ArgumentParser() parser.add_argument("--platform", choices=("linux", "windows")) parser.add_argument("--build", type=Path) + parser.add_argument("--version") parser.add_argument("--docs", type=Path) parser.add_argument("--output", required=True, type=Path) args = parser.parse_args() if args.docs is not None: - if args.platform is not None or args.build is not None: - parser.error("--docs cannot be combined with --platform or --build") + if args.platform is not None or args.build is not None or args.version is not None: + parser.error("--docs cannot be combined with --platform, --build or --version") collect_docs(args.docs, args.output) else: - if args.platform is None or args.build is None: - parser.error("--platform and --build are required when --docs is not specified") - package(args.platform, args.build, args.output) + if args.platform is None or args.build is None or args.version is None: + parser.error("--platform, --build and --version are required when --docs is not specified") + package(args.platform, args.build, args.output, args.version) if __name__ == "__main__": From 141b7d7ff023078d52a87ab38610b7c52b6f03fa Mon Sep 17 00:00:00 2001 From: boykopovar Date: Tue, 29 Sep 2026 00:38:21 +0300 Subject: [PATCH 38/46] fix(ci): use git bash for windows release --- .github/workflows/release.yml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aa0ec6803..522ada2fd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -65,11 +65,26 @@ jobs: if: matrix.platform == 'windows' shell: cmd run: cmake -S . -B build %CONFIGURE% - - name: Build project and tests + - name: Build project and tests (Linux) + if: matrix.platform == 'linux' run: cmake --build build - - name: Run tests + - name: Build project and tests (Windows) + if: matrix.platform == 'windows' + shell: cmd + run: cmake --build build + - name: Run tests (Linux) + if: matrix.platform == 'linux' run: ctest --test-dir build --output-on-failure --timeout 120 --stop-on-failure --no-tests=error - - name: Build patched libraries + - name: Run tests (Windows) + if: matrix.platform == 'windows' + shell: cmd + run: ctest --test-dir build --output-on-failure --timeout 120 --stop-on-failure --no-tests=error + - name: Build patched libraries (Linux) + if: matrix.platform == 'linux' + run: cmake --build build --target libs + - name: Build patched libraries (Windows) + if: matrix.platform == 'windows' + shell: cmd run: cmake --build build --target libs - name: Package release files run: python tools/package_release.py --platform "${{ matrix.platform }}" --build build --output dist --version "$RELEASE_TAG" From ee57519cb0bad5136f37188f9b57746ed0dc52a2 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Tue, 29 Sep 2026 07:39:01 +1000 Subject: [PATCH 39/46] fix(relinker): decode EMMS without a ModRM byte --- .../relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp | 1 + core/relinker/codegen/src/x86/X64InstructionDecoder.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp index 2472fe258..022721a23 100644 --- a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp +++ b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp @@ -130,6 +130,7 @@ inline constexpr std::uint8_t TwoByteModRmRangeFMin = 0x38; inline constexpr std::uint8_t TwoByteModRmRangeFMax = 0x3A; inline constexpr std::uint8_t TwoByteModRmRangeGMin = 0x54; inline constexpr std::uint8_t TwoByteModRmRangeGMax = 0x7F; +inline constexpr std::uint8_t TwoByteEmms = 0x77; inline constexpr std::uint8_t TwoByteModRmRangeHMin = 0xD0; inline constexpr std::uint8_t TwoByteModRmRangeHMax = 0xFE; inline constexpr std::uint8_t TwoByteNopModRm = 0x1F; diff --git a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp index fe0ee7ef9..462ff5c2d 100644 --- a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp @@ -252,7 +252,7 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t (opcode >= TwoByteModRmRangeCMin && opcode <= TwoByteModRmRangeCMax) || (opcode >= TwoByteModRmRangeDMin && opcode <= TwoByteModRmRangeDMax) || (opcode >= TwoByteModRmRangeEMin && opcode <= TwoByteModRmRangeEMax) || - (opcode >= TwoByteModRmRangeGMin && opcode <= TwoByteModRmRangeGMax) || + (opcode >= TwoByteModRmRangeGMin && opcode <= TwoByteModRmRangeGMax && opcode != TwoByteEmms) || (opcode >= TwoByteModRmRangeHMin && opcode <= TwoByteModRmRangeHMax) || (opcode >= TwoByteModRmRangeIMin && opcode <= TwoByteModRmRangeIMax) || (opcode >= TwoBytePrefetchGrpMin && opcode <= TwoBytePrefetchGrpMax) || From 96b622c6ac945fe539b1220bc02f6c43e9cfb32d Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Tue, 29 Sep 2026 07:39:01 +1000 Subject: [PATCH 40/46] feat(libc): add allocating strings and C locale wide conversion --- core/libs/prx/libc/CMakeLists.txt | 2 + core/libs/prx/libc/src/AllocatingStrings.cpp | 82 ++++++++++++++++++++ core/libs/prx/libc/src/WideConversion.cpp | 19 +++++ 3 files changed, 103 insertions(+) create mode 100644 core/libs/prx/libc/src/AllocatingStrings.cpp create mode 100644 core/libs/prx/libc/src/WideConversion.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index ba4ae9c93..88e49d7ac 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -5,6 +5,8 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/windows/CrashReport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/linux/DlIteratePhdr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Strings.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/AllocatingStrings.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/WideConversion.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/StringExtensions.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Math.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Time.cpp diff --git a/core/libs/prx/libc/src/AllocatingStrings.cpp b/core/libs/prx/libc/src/AllocatingStrings.cpp new file mode 100644 index 000000000..d3539587b --- /dev/null +++ b/core/libs/prx/libc/src/AllocatingStrings.cpp @@ -0,0 +1,82 @@ +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include "prx/libc/include/WindowsFormatting.hpp" +#endif + +extern "C" { + +char* APS5_VABI strndup_nid_postfix(const char* source, std::size_t limit) { + std::size_t length = 0; + while (length < limit && source[length] != '\0') ++length; + try { + auto* result = static_cast(ApplicationHeapAllocate_nid_no_patch(length + 1)); + if (!result) { errno = 12; return nullptr; } + std::memcpy(result, source, length); + result[length] = '\0'; + return result; + } catch (const std::bad_alloc&) { + errno = 12; + return nullptr; + } +} + +int APS5_VABI asprintf_nid_postfix(char** destination, const char* format, ...) { + if (!destination) { errno = 22; return -1; } + *destination = nullptr; + if (!format) { errno = 22; return -1; } +#ifdef _WIN32 + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); +#else + std::va_list args; + va_start(args, format); +#endif + int result = -1; + try { +#ifdef _WIN32 + std::string text; + const int count = LibcDetail::FormatWindows(nullptr, 0, format, args, &text); + const char* source = text.c_str(); +#else + char* text = nullptr; + const int count = ::vasprintf(&text, format, args); + std::unique_ptr owner(text, std::free); + const char* source = text; +#endif + if (count >= 0) { + auto* output = static_cast(ApplicationHeapAllocate_nid_no_patch(static_cast(count) + 1)); + if (output) { + std::memcpy(output, source, static_cast(count) + 1); + *destination = output; + result = count; + } else errno = 12; + } + } catch (const std::bad_alloc&) { + errno = 12; + } catch (...) { +#ifdef _WIN32 + __builtin_sysv_va_end(args); +#else + va_end(args); +#endif + throw; + } +#ifdef _WIN32 + __builtin_sysv_va_end(args); +#else + va_end(args); +#endif + return result; +} + +} diff --git a/core/libs/prx/libc/src/WideConversion.cpp b/core/libs/prx/libc/src/WideConversion.cpp new file mode 100644 index 000000000..085d945d5 --- /dev/null +++ b/core/libs/prx/libc/src/WideConversion.cpp @@ -0,0 +1,19 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" std::size_t APS5_VABI wcstombs_nid_postfix(char* destination, const std::uint16_t* source, std::size_t capacity) { + std::size_t count = 0; + while (!destination || count < capacity) { + const auto value = source[count]; + if (value > 255) { + errno = 86; + return static_cast(-1); + } + if (destination) destination[count] = static_cast(value); + if (value == 0) break; + ++count; + } + return count; +} From 5e708c16081c73cc0c6f1438fbe34daa0c76c7dc Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Tue, 29 Sep 2026 07:39:01 +1000 Subject: [PATCH 41/46] feat(kernel): add native pthread barriers and thread helpers --- core/libs/prx/libkernel/CMakeLists.txt | 1 + .../prx/libkernel/Pthread/Posix/Barrier.cpp | 61 +++++++++++++++++++ .../prx/libkernel/Pthread/Posix/Thread.cpp | 9 +++ 3 files changed, 71 insertions(+) create mode 100644 core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 801440466..a173504cb 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -20,6 +20,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Rwlock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Sem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Attr.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Barrier.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Cond.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Mutex.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Rwlock.cpp diff --git a/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp b/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp new file mode 100644 index 000000000..723fd1267 --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp @@ -0,0 +1,61 @@ +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include + +struct PthreadBarrierPrivate { + std::mutex mutex; + std::condition_variable changed; + unsigned count; + unsigned arrived = 0; + unsigned active = 0; + std::uint64_t generation = 0; +}; + +extern "C" { + +int APS5_VABI pthread_barrier_init_nid_postfix(PthreadBarrierPrivate** barrier, const void* attr, unsigned count) { + if (!barrier || count == 0) return 22; + if (attr) NotImplemented_nid_no_patch("pthread_barrier_init: non-default attributes"); + try { + auto* value = new PthreadBarrierPrivate; + value->count = count; + *barrier = value; + return 0; + } catch (const std::bad_alloc&) { + return 12; + } +} + +int APS5_VABI pthread_barrier_wait_nid_postfix(PthreadBarrierPrivate** barrier) { + if (!barrier || !*barrier) return 22; + auto& value = **barrier; + std::unique_lock lock(value.mutex); + const auto generation = value.generation; + ++value.active; + if (++value.arrived == value.count) { + value.arrived = 0; + ++value.generation; + value.changed.notify_all(); + --value.active; + return -1; + } + value.changed.wait(lock, [&] { return value.generation != generation; }); + --value.active; + return 0; +} + +int APS5_VABI pthread_barrier_destroy_nid_postfix(PthreadBarrierPrivate** barrier) { + if (!barrier || !*barrier) return 22; + auto* value = *barrier; + { + std::lock_guard lock(value->mutex); + if (value->active != 0) return 16; + } + delete value; + *barrier = nullptr; + return 0; +} + +} diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index f1d43aa31..fc9fd94d8 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -57,6 +57,15 @@ Pthread APS5_VABI pthread_self_nid_postfix(void) { return scePthreadSelf(); } +int APS5_VABI pthread_equal_nid_postfix(Pthread first, Pthread second) { + return first == second; +} + +int APS5_VABI sched_yield_nid_postfix(void) { + std::this_thread::yield(); + return 0; +} + int APS5_VABI pthread_setcancelstate_nid_postfix(int state, int* old_state) { return PosixThread::ToErrno(scePthreadSetcancelstate(state, old_state)); } From 02b431eb6d778a8fb6853766181ea69a0ae85407 Mon Sep 17 00:00:00 2001 From: asimplestray <304690168+asimplestray@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:15:25 +0000 Subject: [PATCH 42/46] feat(libc): implement sceLibcMspaceReallocalign --- core/libs/prx/libc/src/Mspace.cpp | 25 +++++++++++++++++++++++++ core/libs/tests/Mspace.cpp | 10 ++++++++++ 2 files changed, 35 insertions(+) diff --git a/core/libs/prx/libc/src/Mspace.cpp b/core/libs/prx/libc/src/Mspace.cpp index c718fed93..7821927a8 100644 --- a/core/libs/prx/libc/src/Mspace.cpp +++ b/core/libs/prx/libc/src/Mspace.cpp @@ -212,6 +212,31 @@ void* APS5_VABI sceLibcMspaceMemalign_nid_postfix(void* handle, std::size_t alig return Allocate(Find(handle), size, std::max(alignment, 16)); } +void* APS5_VABI sceLibcMspaceReallocalign_nid_postfix(void* handle, void* pointer, std::size_t size, std::size_t alignment) { + if (alignment == 0 || (alignment & (alignment - 1))) { + Error(22); + return nullptr; + } + const auto effective = std::max(alignment, 16); + std::lock_guard lock(arenaMutex); + auto* arena = Find(handle); + if (!pointer) return Allocate(arena, size, effective); + auto* block = FindBlock(arena, pointer); + if (!block) return nullptr; + if (!size) { Release(arena, block); return nullptr; } + const auto padding = reinterpret_cast(pointer) - reinterpret_cast(block + 1); + if (size <= block->capacity - padding && (reinterpret_cast(pointer) & (effective - 1)) == 0) { + block->used = size; + return pointer; + } + void* result = Allocate(arena, size, effective); + if (result) { + std::memcpy(result, pointer, std::min(block->used, size)); + Release(arena, block); + } + return result; +} + int APS5_VABI sceLibcMspaceMallocStats_nid_postfix(void* handle, MallocManagedSize* stats) { return FillStats(handle, stats); } diff --git a/core/libs/tests/Mspace.cpp b/core/libs/tests/Mspace.cpp index 2cb6143ce..3646992bd 100644 --- a/core/libs/tests/Mspace.cpp +++ b/core/libs/tests/Mspace.cpp @@ -17,6 +17,7 @@ void APS5_VABI sceLibcMspaceFree_nid_postfix(void*, void*); int APS5_VABI sceLibcMspacePosixMemalign_nid_postfix(void*, void**, std::size_t, std::size_t); std::size_t APS5_VABI sceLibcMspaceMallocUsableSize_nid_postfix(const void*); void* APS5_VABI sceLibcMspaceMemalign_nid_postfix(void*, std::size_t, std::size_t); +void* APS5_VABI sceLibcMspaceReallocalign_nid_postfix(void*, void*, std::size_t, std::size_t); int APS5_VABI sceLibcMspaceMallocStats_nid_postfix(void*, void*); int APS5_VABI sceLibcMspaceMallocStatsFast_nid_postfix(void*, void*); } @@ -79,6 +80,15 @@ int main() { void* memaligned = sceLibcMspaceMemalign_nid_postfix(arena, 256, 100); Require(memaligned && (reinterpret_cast(memaligned) & 255) == 0); Require(sceLibcMspaceMemalign_nid_postfix(arena, 24, 100) == nullptr); + unsigned char* realigned = static_cast(sceLibcMspaceReallocalign_nid_postfix(arena, nullptr, 64, 64)); + Require(realigned && (reinterpret_cast(realigned) & 63) == 0); + for (int i = 0; i < 64; ++i) realigned[i] = static_cast(i + 1); + unsigned char* regrown = static_cast(sceLibcMspaceReallocalign_nid_postfix(arena, realigned, 256, 64)); + Require(regrown && (reinterpret_cast(regrown) & 63) == 0); + for (int i = 0; i < 64; ++i) Require(regrown[i] == static_cast(i + 1)); + Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, 16, 0) == nullptr); + Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, 16, 3) == nullptr); + Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, 0, 16) == nullptr); Require(sceLibcMspaceMallocStatsFast_nid_postfix(arena, &stats) == 0 && stats.currentInuseSize >= 100); sceLibcMspaceFree_nid_postfix(arena, memaligned); void* region = sceLibcMspaceMalloc_nid_postfix(arena, 8192); From 6742ecf70f91a35a7da537c308d23bdea9276c2e Mon Sep 17 00:00:00 2001 From: boykopovar Date: Tue, 29 Sep 2026 02:05:02 +0300 Subject: [PATCH 43/46] fix(relinker): use app0 guest module layout and clarify output diagnostics --- core/relinker/main.cpp | 12 ++++++++++++ .../relinker/src/guest/GuestModuleBuilder.cpp | 14 +++++--------- core/relinker/relinker/tests/test_empty_tls.py | 2 +- .../relinker/tests/test_linux_load_alignment.py | 12 +++++++++++- core/relinker/relinker/tests/test_optional_plt.py | 2 +- .../relinker/tests/test_tls_function_coverage.py | 2 +- core/relinker/relinker/tests/test_windows_gui.py | 6 +++--- 7 files changed, 34 insertions(+), 16 deletions(-) diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index f36f383c3..705c3f6b0 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -36,6 +36,9 @@ int main(const int argc, char* argv[]) { } try { + auto extension = std::filesystem::path(args.outputPath).extension().string(); + for (auto& character : extension) if (character >= 'A' && character <= 'Z') character = static_cast(character + ('a' - 'A')); + if (!args.toWindows && extension == ".exe") std::cerr << "WARNING: Output filename ends with .exe, but --windows was not specified. The output will be a Linux ELF executable.\n"; Io::FileReader fileReader; Io::FileWriter fileWriter; @@ -111,6 +114,15 @@ int main(const int argc, char* argv[]) { } fileWriter.Write(absPath, executableBytes); std::cout << "External prx references: " << result.RegistryEntries.size() << "\nOutput file: " << absPath << '\n'; + std::cout << "Expected runtime layout (relative to the output executable):\n" + << std::filesystem::path(absPath).filename().string() << "\n" + << "libs/\n *.prx\napp0/\n \n"; + if (!guestArtifacts.empty()) { + std::cout << " " << guestArtifacts.front().Path.parent_path().filename().string() << "/\n"; + for (const auto& artifact : guestArtifacts) std::cout << " " << artifact.Path.filename().string() << '\n'; + } + std::cout << "Game resources and system libraries must be placed in this layout separately.\n"; + if (args.runPath != "$ORIGIN/libs") std::cout << "Custom library search path (--rpath): " << args.runPath << '\n'; if (args.autorun) return Cli::Autorun(absPath, args.toWindows); diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 4c4c92fea..a8655e1e4 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -18,10 +18,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path const bool hasSingular = std::filesystem::exists(singular); const bool hasPlural = std::filesystem::exists(plural); if (hasSingular && hasPlural) throw Domain::RelinkerException("Both sce_module and sce_modules exist beside the input executable"); - if (!hasSingular && !hasPlural) { - std::cout << "WARNING: sce_module not found beside the input executable; it may be unnecessary or missing.\n"; - return {}; - } + if (!hasSingular && !hasPlural) throw Domain::RelinkerException("sce_module/sce_modules was not found beside the input executable: " + root.string() + ". Use --skip-sce-module only if this game can run without these modules."); const auto directory = hasSingular ? singular : plural; if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + directory.string()); std::vector paths; @@ -119,18 +116,17 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path Io::AppendU64(dynamic.DynamicSegmentData, dynamic.DynStrData.size()); Io::AppendString(dynamic.DynStrData, name); }; - const auto relativeDirectory = directory.filename().generic_string(); + const auto relativeDirectory = "app0/" + directory.filename().generic_string(); for (const auto index : order) if (!windows) addNeeded("$ORIGIN/" + relativeDirectory + "/" + images[index].OutputName); for (const auto& name : hostLibraries) addNeeded(name); std::string guestRunPath = runPath; if (!windows) { - if (guestRunPath == "$ORIGIN") guestRunPath = "$ORIGIN/.."; - else if (guestRunPath.starts_with("$ORIGIN/")) guestRunPath.insert(8, "../"); + if (guestRunPath == "$ORIGIN") guestRunPath = "$ORIGIN/../.."; + else if (guestRunPath.starts_with("$ORIGIN/")) guestRunPath.insert(8, "../../"); else if (!std::filesystem::path(guestRunPath).is_absolute()) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); } std::vector artifacts; - const auto destination = std::filesystem::absolute(outputPath).parent_path() / directory.filename(); - if (std::filesystem::exists(destination) && std::filesystem::equivalent(destination, directory)) throw Domain::RelinkerException("Guest output directory must differ from the source module directory"); + const auto destination = std::filesystem::absolute(outputPath).parent_path() / relativeDirectory; for (const auto index : order) { const auto& image = images[index]; const auto target = destination / image.OutputName; diff --git a/core/relinker/relinker/tests/test_empty_tls.py b/core/relinker/relinker/tests/test_empty_tls.py index aaf0c633e..c92deba32 100644 --- a/core/relinker/relinker/tests/test_empty_tls.py +++ b/core/relinker/relinker/tests/test_empty_tls.py @@ -29,7 +29,7 @@ def main(): if access: data[0x200:0x20a] = bytes.fromhex("64 48 8b 04 25 00 00 00 00 c3") source.write_bytes(data) - result = subprocess.run([str(relinker), "--windows", str(source), str(output)], + result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", str(source), str(output)], capture_output=True, text=True, timeout=20) if error: assert result.returncode == 2 and error in result.stderr and not output.exists(), result diff --git a/core/relinker/relinker/tests/test_linux_load_alignment.py b/core/relinker/relinker/tests/test_linux_load_alignment.py index 2e2d8afaf..bcb3ebaec 100644 --- a/core/relinker/relinker/tests/test_linux_load_alignment.py +++ b/core/relinker/relinker/tests/test_linux_load_alignment.py @@ -41,7 +41,11 @@ def main(): source = Path(directory) / "input.elf" output = Path(directory) / "output.elf" source.write_bytes(fixture()) - result = subprocess.run([str(relinker), str(source), str(output)], capture_output=True, text=True, timeout=20) + missing = subprocess.run([str(relinker), str(source), str(output)], capture_output=True, text=True, timeout=20) + assert missing.returncode != 0 and not output.exists(), missing + assert "sce_module/sce_modules was not found" in missing.stderr, missing.stderr + assert "--skip-sce-module only if this game" in missing.stderr, missing.stderr + result = subprocess.run([str(relinker), "--skip-sce-module", str(source), str(output)], capture_output=True, text=True, timeout=20) if result.returncode != 0: raise AssertionError((result.returncode, result.stdout, result.stderr)) segments = loads(output.read_bytes()) @@ -49,6 +53,12 @@ def main(): alignment = max(segment[7] for segment in segments) if alignment != 0x4000 or first[3] % alignment != 0 or first[7] != alignment: raise AssertionError(("first PT_LOAD is not aligned to the largest segment alignment", segments)) + assert "output.elf\nlibs/\n *.prx\napp0/" in result.stdout, result.stdout + named_exe = Path(directory) / "linux.EXE" + warning = subprocess.run([str(relinker), "--skip-sce-module", str(source), str(named_exe)], capture_output=True, text=True, timeout=20) + assert warning.returncode == 0, (warning.stdout, warning.stderr) + assert "--windows was not specified" in warning.stderr, warning.stderr + assert named_exe.read_bytes().startswith(b"\x7fELF"), "The filename must not select the output format" print("Linux load alignment test passed") diff --git a/core/relinker/relinker/tests/test_optional_plt.py b/core/relinker/relinker/tests/test_optional_plt.py index 894b47a7b..548197caa 100644 --- a/core/relinker/relinker/tests/test_optional_plt.py +++ b/core/relinker/relinker/tests/test_optional_plt.py @@ -38,7 +38,7 @@ def main(): source = work / (name + ".elf") output = work / (name + ".exe") source.write_bytes(fixture(tags)) - result = subprocess.run([str(relinker), "--windows", str(source), str(output)], + result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", str(source), str(output)], capture_output=True, text=True, timeout=20) if error is not None: if result.returncode != 2 or error not in result.stderr or output.exists(): diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index c0bc8145b..afc35a9ab 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -78,7 +78,7 @@ def main(): source = work / (name + ".elf") output = source.with_suffix(".exe") source.write_bytes(image) - result = subprocess.run([str(relinker), "--windows", str(source), str(output)], + result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", str(source), str(output)], capture_output=True, text=True, timeout=30) if error is not None: assert result.returncode == 2 and error in result.stderr and not output.exists(), result diff --git a/core/relinker/relinker/tests/test_windows_gui.py b/core/relinker/relinker/tests/test_windows_gui.py index 12698e829..f7dbc2b83 100644 --- a/core/relinker/relinker/tests/test_windows_gui.py +++ b/core/relinker/relinker/tests/test_windows_gui.py @@ -22,19 +22,19 @@ def main(): source.write_bytes(fixture()) cui_output = work / "cui.exe" - result = subprocess.run([str(relinker), "--windows", str(source), str(cui_output)], + result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", str(source), str(cui_output)], capture_output=True, text=True, timeout=20) assert result.returncode == 0, (result.stdout, result.stderr) assert subsystem(cui_output.read_bytes()) == 3, "expected IMAGE_SUBSYSTEM_WINDOWS_CUI by default" gui_output = work / "gui.exe" - result = subprocess.run([str(relinker), "--windows", "--windows-gui", str(source), str(gui_output)], + result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", "--windows-gui", str(source), str(gui_output)], capture_output=True, text=True, timeout=20) assert result.returncode == 0, (result.stdout, result.stderr) assert subsystem(gui_output.read_bytes()) == 2, "expected IMAGE_SUBSYSTEM_WINDOWS_GUI with --windows-gui" bad_output = work / "bad.exe" - result = subprocess.run([str(relinker), "--windows-gui", str(source), str(bad_output)], + result = subprocess.run([str(relinker), "--skip-sce-module", "--windows-gui", str(source), str(bad_output)], capture_output=True, text=True, timeout=20) assert result.returncode != 0 and not bad_output.exists(), result print("Windows GUI subsystem integration tests passed") From c8beb90a5e40259d53ad27dfa2566395f91d4a03 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Tue, 29 Sep 2026 02:13:22 +0300 Subject: [PATCH 44/46] build(release): package libraries under libs subfolder --- tools/package_release.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/package_release.py b/tools/package_release.py index 86209fb60..25296672b 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -25,12 +25,12 @@ def package(platform, build, output, version): if not file.is_file() or file.stat().st_size == 0: raise RuntimeError(f"Missing or empty release file: {file}") output.mkdir(parents=True, exist_ok=True) - with zipfile.ZipFile(output / f"prx-{platform}-{version}.zip", "w", compression=zipfile.ZIP_DEFLATED) as archive: + with zipfile.ZipFile(output / f"prx-{platform}-{version}.zip", "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive: for file in files: - archive.write(file, arcname=file.name) - with tarfile.open(output / f"prx-{platform}-{version}.tar.gz", "w:gz") as archive: + archive.write(file, arcname=f"libs/{file.name}") + with tarfile.open(output / f"prx-{platform}-{version}.tar.gz", "w:gz", compresslevel=9) as archive: for file in files: - archive.add(file, arcname=file.name) + archive.add(file, arcname=f"libs/{file.name}") asset = f"relinker-{version}.exe" if platform == "windows" else f"relinker-{version}" shutil.copy2(binary, output / asset) From 7445c6599418aefe8e16f69045e78a38bcda3835 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Tue, 29 Sep 2026 02:17:56 +0300 Subject: [PATCH 45/46] chore(readme): return percent calculation info --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4b658c471..9d2bdc641 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ Includes a [relinker](core/relinker) that converts executable to the target syst [![progress map](https://boykopovar.github.io/AnyPS5/progress.svg)](https://boykopovar.github.io/AnyPS5/) +* System libraries: percentage of the functions known to the project so far (declared in [core/libs/prx](core/libs/prx)), not of every PS5 system function. The total grows as more functions are declared. + [List of verified games](docs/user/COMPATIBILITY.md) Dreaming Sarah (2D platformer) runs at a stable 60 fps on a GTX 1050 Ti / i5-7500 3.4GHz. From b01e0bbf5276a778186d7a8bc5aaad6a71007df5 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Tue, 29 Sep 2026 02:24:56 +0300 Subject: [PATCH 46/46] fix(build): use portable save paths and validate all PRX libraries in CI --- .github/workflows/build.yml | 3 +++ core/libs/prx/libSceSaveData/SaveDataCore.hpp | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5b07dfadd..c40f7ec45 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,6 +34,7 @@ jobs: evict-old-files: job - run: cmake -S . -B build $CONFIGURE - run: cmake --build build + - run: cmake --build build --target libs - run: ctest --test-dir build --output-on-failure --timeout 120 windows: @@ -68,5 +69,7 @@ jobs: run: cmake -S . -B build %CONFIGURE% - shell: cmd run: cmake --build build + - shell: cmd + run: cmake --build build --target libs - shell: cmd run: ctest --test-dir build --output-on-failure --timeout 120 diff --git a/core/libs/prx/libSceSaveData/SaveDataCore.hpp b/core/libs/prx/libSceSaveData/SaveDataCore.hpp index 166176bfe..e72aa4c77 100644 --- a/core/libs/prx/libSceSaveData/SaveDataCore.hpp +++ b/core/libs/prx/libSceSaveData/SaveDataCore.hpp @@ -290,7 +290,8 @@ std::filesystem::path save_blocks_path(const std::filesystem::path& directory) { bool write_blob(const std::filesystem::path& path, const void* data, std::size_t size) { std::error_code error; std::filesystem::create_directories(path.parent_path(), error); - const std::filesystem::path temporary = path.native() + L".tmp"; + auto temporary = path; + temporary += ".tmp"; { std::ofstream file(temporary, std::ios::binary | std::ios::trunc); if (!file) {